5.4 Επικοινωνία με Βάσεις Δεδομένων
© Γιάννης Κωστάρας
<- | Δ | -> |
Στα προηγούμενα μαθήματα είδαμε πώς μπορούμε ν’ αποθηκεύουμε μόνιμα τα δεδομένα μας σε συστήματα αρχείων. Σ’ αυτό το μάθημα θα μάθουμε πώς ν’ αποθηκεύουμε τα δεδομένα μας σε βάσεις δεδομένων.
Κατηγορίες Βάσεων Δεδομένων
Υπάρχουν διάφορες κατηγορίες βάσεων δεδομένων:
- Σχεσιακές Βάσεις Δεδομένων
- Αντικειμενοστραφής Βάσεις Δεδομένων
- XML Βάσεις Δεδομένων
- NoSQL Βάσεις Δεδομένων
Σχεσιακές Βάσεις Δεδομένων
Το πιο παραδοσιακό μοντέλο είναι οι σχεσιακές βάσεις δεδομένων (Σχεσιακά Συστήματα Διαχείρισης Βάσεων Δεδομένων - ΣΣΔΒΔ ή Relational Database Management Systems - RDBMS). Σ’ αυτές, τα δεδομένα αποθηκεύονται σε πίνακες οι οποίοι σχετίζονται μεταξύ τους. Η επικοινωνία μ’ αυτές γίνεται με τη 4ης γενιάς γλώσσα Structured Query Language (SQL). Οι εντολές της SQL χωρίζονται στις εξής δυο κατηγορίες:
- Γλώσσα Ορισμού Δεδομένων (Data Definition Language - DDL):
CREATE (DROP) TABLE/VIEW/INDEX, ALTER TABLE
- Γλώσσα Διαχείρισης Δεδομένων (Data Manipulation Language - DML):
SELECT, INSERT, UPDATE, DELETE
Τα πιο δημοφιλή ΣΣΔΒΔ είναι τα ακόλουθα:
Η Java μπορεί να επικοινωνήσει με ΣΣΔΒΔ για να επεξεργαστεί δεδομένα που είναι αποθηκευμένα σ’ αυτά, χάρις στο Java Database Connectivity Bridge (JDBC) (τελευταία έκδοση 4.3). Η ιδέα πίσω από το JDBC είναι ότι κάθε κατασκευαστής προσφέρει τον οδηγό του (driver) που υλοποιεί το JDBC.
Εικόνα 5.4.1 Java Database Connectivity Bridge (JDBC)
Επικοινωνία με Σχεσιακές ΒΔ
Στα παρακάτω θα χρησιμοποιήσουμε την SQLite ως ΣΔΒΔ και θα δημιουργήσουμε το σχήμα (schema) της ακόλουθης ΒΔ η οποία αποτελείται από έναν πίνακα Users
:
Εικόνα 5.4.2 Πίνακας Users
Κατεβάστε τα:
Με τον SQLiteBrowser δημιουργήστε μια νέα ΒΔ με όνομα UserDB.sqlite3
και τον πίνακα Users
όπως φαίνεται στην ακόλουθη εικόνα:
Εικόνα 5.4.3 Δημιουργία πίνακα Users με τον SQLiteBrowser
Συγχαρητήρια! Μόλις δημιουργήσατε μια SQLite3 ΒΔ. Μπορείτε να εισάγετε κάποιες εγγραφές σ’ αυτήν είτε επιλέγοντας την καρτέλα Browser Data και πατώντας το κουμπί New Record και εισάγοντας τιμές για τα πεδία username, password
, είτε επιλέγοντας την καρτέλα Execute SQL και εισάγοντας μια εντολή SQL όπως η παρακάτω:
INSERT INTO Users (username, password) VALUES ('user', 'user');
Ας δούμε τώρα πώς μπορούμε να δημιουργήσουμε την ίδια ΒΔ με το NetBeans και πώς μπορούμε να επικοινωνήσουμε μ’ αυτή.
- Κάντε κλικ στο μενού Window –> Services για να εμφανίσετε το παράθυρο Services
- Εμφανίστε τον κόμβο Databases και κάντε δεξί κλικ στον κόμβο Drivers και επιλέξτε New Driver
- Στο διαλογικό παράθυρο New JDBC Driver που εμφανίζεται, πατήστε το κουμπί Add… και επιλέξτε το αρχείο
sqlite-jdbc-3.xx.xx.jar
που κατεβάσατε. Ως όνομα δώστεSQLite3
- Κάντε δεξί κλικ στον κόμβο Databases και επιλέξτε New Connection. Στο διαλογικό παράθυρο που εμφανίζεται, επιλέξτε τον οδηγό SQLite3 που δημιουργήσατε στο προηγούμενο βήμα και πατήστε Next.
- Προαιρετικά δώστε ένα User Name και Password για να συνδεθείτε στη ΒΔ, και ως JDBC URL δώστε τη διαδρομή που θέλετε ν’ αποθηκεύσετε τη ΒΔ (ή τη διαδρομή της ΒΔ που δημιουργήσαμε προηγούμενα):
jdbc:sqlite:<path>/UserDB.sqlite3
- Πατήστε Finish
Το NetBeans δημιούργησε μια νέα σύνδεση (Connection) στη ΒΔ που δημιουργήσατε που περιέχει 3 κόμβους: Tables, Views, Procedures. Μπορείτε να δημιουργήσετε πίνακες, να δείτε τα δεδομένα τους, να εισάγετε νέα δεδομένα κλπ.
Εικόνα 5.4.4 Πίνακας Users στο NetBeans
Στη συνέχεια θα δημιουργήσουμε ένα πρόγραμμα Java για να επικοινωνήσουμε με τη ΒΔ UserDB.sqlite3
.
- Στο NetBeans, δημιουργήστε ένα νέο έργο Java Application και δώστε ένα όνομα στην εφαρμογή, π.χ.
UserDBApp
- Επιλέξτε τα Use Dedicated Folder for Storing Libraries και Create Main Class, αφήστε τα προτεινόμενα ονόματα και πατήστε το Finish
- Δεξί κλικ στο Libraries και Add JAR/Folder. Επιλέξτε το
sqlite-jdbc-3.xx.xx.jar
και Copy to Libraries Folder και στη συνέχεια το κουμπί Choose - Στη
main()
μέθοδο της κλάσης εισάγετε τον εξής κώδικα:
import java.sql.*;
public class UserDBApp {
private static final String URL = "jdbc:sqlite:<path_to>/UserDB.sqlite3";
private static final String DB_ADMIN_USERNAME = "admin";
private static final String DB_ADMIN_PASSWORD = "admin";
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Connection dbConnection = getConnection();
}
/**
* @return connection to database
*/
public static Connection getConnection() {
Connection connection = null;
try {
// create a connection to the database
connection = DriverManager.getConnection(URL, DB_ADMIN_USERNAME, DB_ADMIN_PASSWORD);
} catch (SQLException e) {
System.err.println(e.getMessage());
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
System.err.println(e.getMessage());
}
}
return connection;
}
}
Η μέθοδος getConnection()
δημιουργεί μια σύνδεση (Connection
) με τη ΒΔ που ορίζεται με το URL
. Το όνομα χρήστη και ο κωδικός του διαχειριστή της ΒΔ δεν απαιτούνται από την SQLite, αλλά απαιτούνται από άλλες ΒΔ όπως π.χ. MySQL κλπ. Προσοχή, τα DB_ADMIN_USERNAME, DB_ADMIN_PASSWORD
χρησιμοποιούνται για να συνδεθούμε με το ΣΔΒΔ της SQLite και δεν έχουν καμία σχέση με τα περιεχόμενα του πίνακα Users
. Παρατηρήστε ότι αν συμβεί κάποιο λάθος δε θέλουμε το connection
να παραμείνει ανοικτό.
Ας δούμε πώς μπορούμε να διαβάσουμε τα περιεχόμενα του πίνακα Users
:
//...
private static final String SQL_SELECT_ALL = "SELECT username, password FROM users ORDER BY username";
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Connection dbConnection = getConnection();
selectAll(dbConnection);
}
/**
* Select all rows in the {@code Users} table
* @param dbConnection database connection
*/
public static void selectAll(Connection dbConnection) {
if (dbConnection != null) {
try (Statement stmt = dbConnection.createStatement();
ResultSet rs = stmt.executeQuery(SQL_SELECT_ALL)) {
// loop through the result set
while (rs.next()) {
System.out.println(rs.getString("username") + "\t"
+ rs.getString("password"));
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
}
//...
Η μέθοδος selectAll()
δημιουργεί μια νέα εντολή (Statement
) με την οποία εκτελεί ένα ερώτημα SQL στη ΒΔ με τη μέθοδο executeQuery
. Το αποτέλεσμα αποθηκεύεται σ’ ένα αντικείμενο τύπου ResultSet
. Στη συνέχεια ανακτούμε ένα ένα τα αποθηκευμένα δεδομένα στην ResultSet
και τα εμφανίζουμε στην οθόνη. Η ResultSet
μοιάζει με έναν Iterator
αλλά διαφέρει αρκετά απ’ αυτόν. Π.χ. δεν υπάρχει μέθοδος hasNext()
, η next()
δείχνει αρχικά πριν από την πρώτη εγγραφή, και πρέπει να την καλέσετε τουλάχιστο μια φορά ώστε να μετακινηθεί στην πρώτη εγγραφή, εκτός κι αν δεν υπάρχει καμία εγγραφή οπότε επιστρέφει false
.
Η executeQuery()
παράγει μια εντολή SQL SELECT
. Για τροποποίηση των δεδομένων (εντολές SQL INSERT, UPDATE, DELETE, CREATE, DROP, ALTER
), χρησιμοποιήστε την executeUpdate()
.
Ανάλογα με τον τύπο των δεδομένων, υπάρχουν διάφορες μέθοδοι ανάκτησης αυτών π.χ. getString(), getDouble()
κλπ. Προσέξτε ότι υπάρχει η getDate()
η οποία επιστρέφει java.sql.Date
(κι όχι java.util.Date
), η getTime()
η οποία επιστρέφει java.sql.Time
και η getTimeStamp()
η οποία επιστρέφει java.sql.TimeStamp
. Θα πρέπει να τις μετατρέψετε στις αντίστοιχες κλάσεις ημερομηνίας και ώρας για να τις χρησιμοποιήσετε στο Java πρόγραμμά σας.
Επίσης, χρησιμοποιήσαμε την try-with-resources για να κλείσουμε αυτόματα τη σύνδεση με την ΒΔ, διαφορετικά θα ‘πρεπε να έχουμε μια ακόμα μέθοδο για να κλείσουμε τη σύνδεση με τη ΒΔ:
/**
* Close connection
*
* @param connection to close
*/
public static void closeConnection(Connection connection) {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException ex) {
System.out.println(ex.getMessage());
}
}
Ας δούμε ένα ακόμα παράδειγμα. Η παρακάτω μέθοδος isValid()
ελέγχει αν ένας χρήστης είναι έγκυρος για σύνδεση στην εφαρμογή ή όχι:
/**
* @param username username
* @param pwd password
* @return {@code true} if the user credentials are valid
*/
public static boolean isValid(String username, String pwd) {
if (username == null || username.isBlank() || pwd == null || pwd.isBlank()) {
return false;
}
final String sqlString
= "SELECT * FROM users WHERE username = '" + username
+ "' AND password = '" + pwd + "'";
try (Connection dbConnection = getConnection();
Statement stmt = dbConnection.createStatement();
ResultSet rs = stmt.executeQuery(sqlString)) {
return rs.next();
} catch (SQLException e) {
System.out.println(e.getMessage());
}
return false;
}
Με τον ίδιο τρόπο όπως και πριν, η executeQuery()
καλεί την κατάλληλη εντολή SQL. Αν αυτή επιστρέψει κάποιο αποτέλεσμα, αυτό σημαίνει ότι υπάρχει κάποια εγγραφή στον πίνακα και άρα η μέθοδος επιστρέφει true
διαφορετικά false
. Έτσι, π.χ. η παρακάτω εντολή επιστρέφει Username: user, Password: user is valid? true
(ή false
) αν ο χρήστης user
με κωδικό user
(δεν) υπάρχει στον πίνακα Users
.
System.out.println("Username: user, Password: user is valid? " + isValid("user", "user"));
Ένας κακόβουλος χρήστης της εφαρμογής όμως δίνει τα εξής διαπιστευτήρια:
Username: user' OR '1'='1
Password: any
τα οποία μεταφράζονται σε:
System.out.println("Username: user' OR '1'='1, Password: any is valid? " + isValid("user' OR '1'='1", "any"));
και λαμβάνει το αποτέλεσμα: Username: user' OR '1'='1, Password: any is valid? true
!
Πώς τα καταφέρνει; Ας δούμε πώς διαμορφώνεται το τελικό sqlString
με αυτές τις παραμέτρους:
SELECT * FROM users WHERE username = 'user' OR '1'='1' AND password = 'any';
Με αυτόν τον τρόπο, όπως καταλαβαίνετε, καταφέρνει να περάσει μια συνθήκη η οποία είναι πάντα αληθής ('1'='1'
) και να ξεγελάσει το πρόγραμμά μας ώστε να παρακάμψει τον έλεγχο και να συνδεθεί στην εφαρμογή μας χωρίς εξουσιοδότηση. Αυτού του είδους η ‘επίθεση’ λέγεται SQL Injection και είναι πολύ συνήθης.
Ευτυχώς, υπάρχει λύση. Χρησιμοποιείτε πάντα PreparedStatement
αντί για Statement
:
/**
* @param username username
* @param pwd password
* @return {@code true} if the user credentials are valid
*/
public static boolean isValid2(String username, String pwd) {
if (username == null || username.isBlank() || pwd == null || pwd.isBlank()) {
return false;
}
final String sqlString
= "SELECT * FROM users WHERE username = ? AND password = ?";
try (Connection dbConnection = getConnection();
PreparedStatement stmt = dbConnection.prepareStatement(sqlString)) {
stmt.setString(1, username);
stmt.setString(2, pwd);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next();
} catch (SQLException e) {
System.out.println(e.getMessage());
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
return false;
}
Προσέξτε ότι στις ΒΔ η πρώτη εγγραφή είναι η 1
(κι όχι η 0
όπως στις συστοιχίες ή στις συλλογές της Java).
Πλέον το SQL Injection δεν έχει καμία πιθανότητα:
System.out.println("Username: user' OR '1'='1, Password: any is valid? " + isValid2("user' OR '1'='1", "any"));
Username: user' OR '1'='1, Password: any is valid? false
Φυσικά, δεν είναι καθόλου καλή τεχνική να αποθηκεύουμε τα διαπιστευτήρια χρηστών ή του διαχειριστή της ΒΔ μέσα στον κώδικά μας καθώς είναι πολύ εύκολο για κάποιον τρίτο να τα βρει:
connection = DriverManager.getConnection(URL, DB_ADMIN_USERNAME, DB_ADMIN_PASSWORD);
Το σωστό είναι να τ’ αποθηκεύσουμε σε κάποιο αρχείο στο οποίο περιορίζουμε την πρόσβαση σε τρίτους ή να τα ζητάμε από τον χρήστη κατά την εκτέλεση του προγράμματος. (Γενικά είναι καλή τεχνική ν’ αποθηκεύουμε όλα τα δεδομένα που απαιτούνται για τη σύνδεση σε μια ΒΔ σ’ ένα αρχείο .properties
και να τα διαβάζουμε από κει, ώστε αν μελλοντικά αλλάξουμε ΣΔΒΔ να μη χρειάζεται να επαναμεταγλωττίσουμε το πρόγραμμά μας).
Επίσης, είναι πολύ κακή τεχνική να αποθηκεύουμε τους κωδικούς ως έχει στη ΒΔ. Όποιος αποκτήσει πρόσβαση στη ΒΔ θα μπορεί να διαβάσει τους κωδικούς των χρηστών της εφαρμογής. Μια συνήθης τεχνική είναι αντί για τους ίδιους τους κωδικούς ν’ αποθηκεύονται τα hash codes αυτών. Μ’ αυτόν τον τρόπο αρκεί να συγκρίνουμε αν το hashCode που περνά ο χρήστης ως διαπιστευτήριο είναι ίδιο με το hashCode που είναι αποθηκευμένο στη ΒΔ.
//...
String hashPassword(char[] password) {
// δημιουργία hash
}
//...
public static boolean isValid2(String username, String pwd) {
//...
String pwdHash = hashPassword(pwd);
//...
stmt.setString(2, pwdHash);
}
Η βιβλιοθήκη java.sql
παρέχει και άλλες δυνατότητες. Π.χ. σας επιτρέπει να αποθηκεύσετε τα αποτελέσματα σε μια προσωρινή μνήμη (cache) ώστε να είναι διαθέσιμα ακόμα και αφού κλείσει η σύνδεση με τη ΒΔ, με τη βοήθεια των συνόλων γραμμών (rowsets):
import javax.sql.rowset.*;
//...
public static void selectAllCached(Connection dbConnection) {
try {
RowSetFactory rsFactory = RowSetProvider.newFactory();
try (CachedRowSet crs = rsFactory.createCachedRowSet()) {
try (Statement stmt = dbConnection.createStatement();
ResultSet rs = stmt.executeQuery(SQL_SELECT_ALL)) {
crs.populate(rs);
// loop through the result set
} catch (SQLException e) {
System.out.println(e.getMessage());
} finally {
dbConnection.close();
}
while (crs.next()) {
System.out.println(crs.getString("username") + "\t"
+ crs.getString("password"));
}
} catch (SQLException ex) {
Logger.getLogger(UserDBApp.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (SQLException ex) {
Logger.getLogger(UserDBApp.class.getName()).log(Level.SEVERE, null, ex);
}
}
Η
try (Statement stmt = dbConnection.createStatement();
ResultSet rs = stmt.executeQuery(SQL_SELECT_ALL)) {
crs.populate(rs);
// loop through the result set
} catch (SQLException e) {
System.out.println(e.getMessage());
} finally {
dbConnection.close();
}
κλείνει τη σύνδεση με τη ΒΔ (αν και η finally
δεν είναι απαραίτητη καθώς χρησιμοποιούμε try-with-resources).
Με τη βοήθεια των RowSetFactory
και CachedRowSet
βλέπουμε ότι μπορούμε ν’ ανακτήσουμε δεδομένα χωρίς να είμαστε συνδεδεμένοι με τη ΒΔ. Υπάρχουν επίσης οι WebRowSet, FilteredRowSet, JoinRowSet
και JdbcRowSet
.
Μπορείτε επίσης να ορίσετε τη μέθοδο crs.setPageSize(20)
για να λάβετε π.χ. μόνο 20 αποτελέσματα (στην περίπτωση που σας επιστρέφονται πολλά αποτελέσματα). Για να μεταβείτε στην επόμενη σελ. αποτελεσμάτων, crs.nextPage()
, ενώ αν τροποποιήσατε τα δεδομένα, θα πρέπει να καλέσετε την crs.acceptChanges()
για να ενημερώσετε τη ΒΔ.
Αν θέλετε να μάθετε περισσότερα για τη δομή (σχήμα) της ΒΔ σας:
DatabaseMetaData metadata = dbConnection.getMetaData();
// (String catalog, String Schema, String tableNamepattern, string type)
metadata.getTables(null, null, null, new String[]{"TABLE"});
ResultSetMetaData metadata = rs.getMetaData();
for (int i = 1; i <= metadata.getColumnCount(); i++) {
System.out.println(metadata.getColumnLabel(i) + "\t" + metadata.getColumnDisplaySize(i));
}
Η PreparedStatement
μας δίνει τη δυνατότητα να εκτελούμε batch updates, δηλ. πολλά updates μαζί, π.χ.
String sql = "INSERT INTO Users (username, password) VALUES (?, ?);";
PreparedStatement stmt = dbConnection.prepareStatement(sql);
stmt.setString(1, 'katerina');
stmt.setString(2, 'thunderkat');
stmt.addBatch();
stmt.setString(1, 'zinovia');
stmt.setString(2, 'zina');
stmt.addBatch();
int[] rowsAffected = stmt.executeBatch();
Τέλος, αν θέλετε να χρησιμοποιήσετε συναλλαγές (transactions) δηλ. να εκτελέσετε μαζικά μια σειρά από SQL queries που θα πρέπει να επιτύχουν όλες (αν αποτύχει έστω και μία τότε αποτυγχάνουν όλες), θα πρέπει να:
dbConnection.setAutoCommit(false);
//...
stmt.executeUpdate(sqlCommand1);
stmt.executeUpdate(sqlCommand2);
//...
dbConnection.commit();
ενώ σε περίπτωση λάθους:
dbConnection.rollback();
Αυτό που κάνουν οι παραπάνω εντολές είναι να απενεργοποιήσουν το AutoCommit και να το καλέσουνε χειροκίνητα. Εξ’ ορισμού το AutoCommit είναι ενεργοποιημένο και γι’ αυτό εκτελούνται οι εντολές executeUpdate
στη ΒΔ.
Μπορείτε ακόμα να χρησιμοποιήσετε CallableStatement
s για κλήση Αποθηκευμένων Διαδικασιών (Stored Procedures) και Save Points αλλά ξεφεύγουν από το σκοπό του εισαγωγικού αυτού μαθήματος.
Μη σχεσιακές ΒΔ
Με την παραγωγή πολλών δεδομένων, εμφανίστηκαν οι περιορισμοί των σχεσιακών ΒΔ. Με τον ορισμό “Υπέρογκα Δεδομένα (Big Data)” εννοούμε μεγάλες ποσότητες δεδομένων που αλλάζουν συχνά και δεν έχουν την ίδια δομή ή μπορεί να είναι αδόμητα (π.χ. μετεωρολογικά/σεισμικά δεδομένα, πωλήσεις κλπ.). Δουλεύοντας με υπέρογκα δεδομένα σημαίνει να μπορούμε να:
- ανακτούμε αποτελεσματικά τα δεδομένα αυτά
- τ’ αποθηκεύουμε αποτελεσματικά και με φθηνούς τρόπους
- επεξεργαζόμαστε τα δεδομένα γρήγορα
- αναλύουμε τ’ αποτελέσματα
Οι ΒΔ που μπορούν ν’ αποθηκεύουν τέτοιου είδους δεδομένα ονομάζονται NoSQL. Συνήθως δεν έχουν κάποιο schema (αδόμητα δεδομένα) όπως οι σχεσιακές ΒΔ. Οι NoSQL ΒΔ χωρίζονται στις παρακάτω κατηγορίες (λίστα με NoSQL ΒΔ):
- Κατακερματισμού (Key–value): ένας μεγάλος πίνακας κατακερματισμού όπου τα δεδομένα διαβάζονται πολύ γρήγορα μέσω του κλειδιού (Redis για επικοινωνία με Java χρησιμοποιήστε το Jedis, Amazon DynamoDB, Microsoft Azure Table Storage, Riak, Oracle NoSQL Database)
- Εγγράφων (Document): ιεραρχικές χωρίς σχήμα όπου κι εδώ είναι key-value με τη διαφορά ότι το value είναι ένα έγγραφο (MongoDB, Amazon DocumentDB, CouchDB, MarkLogic, Terrastore)
- Στήλης (Column): σχετιζόμενα δεδομένα αποθηκεύονται μαζί (Cassandra, Hbase, Accumulo, Amazon DynamoDB, Hypertable)
- Γράφων (Graph): επιτρέπουν ερωτήματα βασισμένα στις σχέσεις μεταξύ των κόμβων (Neo4j, Infinite Graph, FlockDB, Fallen 8)
Επικοινωνία με NoSQL ΒΔ
Ως παράδειγμα, θα χρησιμοποιήσουμε τη MongoDB. Ο παρακάτω πίνακας συγκρίνει τη MongoDB με μια σχεσιακή ΒΔ:
Σχεσιακή ΒΔ | MongoDB |
Database | Database |
Table | Collection |
Row | Document |
Column | Field |
Join | Embedded Documents |
Primary Key | Primary Key (Εξ’ ορισμού key _id παρέχεται από τη MongoDB) |
Κατεβάστε (αν δε θέλετε να εγκαταστήσετε τη MongoDB στον Η/Υ σας τότε μπορείτε να την χρησιμοποιήσετε ως υπηρεσία μέσω του MongoDB Atlas ή του mlab.com):
- τη MongoDB (MongoDB Community Server για το Λ.Σ. σας) και αποσυμπιέστε τη σε κάποιον φάκελο
- το NBMongo plugin και εγκαταστήστε το στο NetBeans κατά τα γνωστά. Με το που θα εγκαταστήσετε το πρόσθετο, στην καρτέλα Services εμφανίζεται ένας νέος κόμβος MongoDB
- το MongoDB Java Driver. Καθώς δεν έχουμε μάθει κάποιο από τα build frameworks όπως τα maven, gradle, πλοηγηθείτε στο maven central και αναζητήστε
mongodb-driver-sync
και κατεβάστε την τελευταία έκδοση. Επίσης θα χρειαστείτε τις βιβλιοθήκεςmongodb-driver-core
καιbson
.
- Εξ’ ορισμού ο διακομιστής MongoDB (
mongod
) δημιουργεί τη ΒΔ στην τοποθεσία (/data/db
). Στα Windows, ενημερώστε το κλειδίdbpath
στο αρχείοmongo.config
με τη διαδρομή που θέλετε να αποθηκεύετε τη ΒΔ. Στο Unix/Linux, εκκινήστε το διακομιστή ως εξής:./mongod --dbpath <path>
όπου θα πρέπει να έχετε ήδη δημιουργήσει το φάκελο, π.χ.../db
(άρα θα αποθηκευθεί στοmongodb-x.x.x/db
). Ο διακομιστής εξυπηρετεί στη θύρα27017
. - Στο NetBeans, κάντε δεξί κλικ στο MongoDB στην καρτέλα Services και επιλέξτε New Connection. Δώστε ένα όνομα, π.χ.
TestMongoDB
, πατήστε το κουμπί...
και δώστε ένα όνομα στη ΒΔ (π.χ.usersdb
) και προαιρετικά Username, Password για περισσότερη ασφάλεια και OK. - Δεξί κλικ στον κόμβο TestMongoDB και Connect.
- Δεξί κλικ στον κόμβο usersdb και Add Collection. Δώστε ως Collection name
users
. Μια ΒΔ τύπου εγγράφων (document) περιέχει συλλογές. - Δεξί κλικ στον κόμβο users και Open.
- Πατήστε το κουμπί Add Document από τη μπάρα εργαλείων και εισάγετε ένα νέο έγγραφο σε μορφή JSON (για την οποία θα μιλήσουμε στο επόμενο μάθημα, μάλιστα για την ακρίβεια αποθηκεύει τα δεδομένα σε δυαδική μορφή JSON ή BSON). Μια συλλογή περιέχει έγγραφα.
{username:'admin', password:'admin'}
Εικόνα 5.4.5 Συλλογή users στο NetBeans
Παρατηρήστε ότι δε χρειάζεται να ορίσουμε κάποιο schema για τη ΒΔ μας (όπως στην περίπτωση των σχεσιακών ΒΔ με την CREATE TABLE
). Οι συλλογές δημιουργούνται την πρώτη φορά που εισάγονται δεδομένα. Το πρωτεύον κλειδί είναι πάντα το _id
. Επίσης, μπορείτε ν’ αλλάξετε δυναμικά το schema της συλλογής, προσθέτοντας π.χ. ένα ακόμα πεδίο:
{ username:'admin', password:'admin', admin:true }
(διαγράψτε την προηγούμενη εγγραφή).
Μπορείτε να αναζητήσετε ένα έγγραφο από το άνω μέρος του παραθύρου, πατώντας Edit και δίνοντας π.χ. το παρακάτω κριτήριο:
{ "username" : "admin" }
Ας δημιουργήσουμε ένα νέο έργο MongoDBApp
(όπως δημιουργήσατε το UserDBApp
) και ας δούμε πώς μπορούμε να επικοινωνήσουμε με τη ΒΔ από ένα πρόγραμμα Java.
Προσθέστε τις βιβλιοθήκες mongodb-driver-core.x.x.x.jar, mongodb-driver-sync-x.x.x.jar, bson.x.x.x.jar
στο έργο αυτό και προσθέστε τον ακόλουθο κώδικα στη main()
:
MongoClient mongoClient = MongoClients.create();
MongoDatabase database = mongoClient.getDatabase("usersdb");
MongoCollection<Document> collection = database.getCollection("users");
System.out.println("Found: " + collection.countDocuments());
Για να επικοινωνήσουμε με το διακομιστή (server) της MongoDB ΒΔ που δημιουργήσαμε, θα χρειαστούμε ένα πρόγραμμα πελάτη (client) MongoClient
μέσω του οποίου μπορούμε ν’ ανακτήσουμε τη ΒΔ μέσω του ονόματός της κι από αυτή τις συλλογές που περιέχει. Η τελευταία εντολή επιστρέφει το σύνολο των εγγράφων που είναι αποθηκευμένα στη συλλογή users
.
Αν θέλουμε να εισάγουμε ένα νέο έγγραφο στη συλλογή μας:
Document doc = new Document("username", "john").append("password", "john");
collection.insertOne(doc);
Αν θέλουμε να εισάγουμε πολλά έγγραφα, τότε μπορούμε να χρησιμοποιήσουμε την insertMany()
παρέχοντάς της μια λίστα από έγγραφα (List<Document>
).
Η αναζήτηση με βάση κάποια κριτήρια γίνεται παρόμοια:
System.out.println(collection.find().first().toJson());
System.out.println("Found: " + collection.find(eq("admin", true)).first().toJson());
MongoCursor<Document> cursor = collection.find().iterator();
try {
while (cursor.hasNext()) {
System.out.println(cursor.next().toJson());
}
} finally {
cursor.close();
}
Πιο πάνω βλέπουμε τρεις τρόπους αναζήτησης, ο πρώτος με χρήση της find()
επιστρέφει το πρώτο έγγραφο, ο δεύτερος παρέχει ένα κριτήριο (eq
) και ο τρίτος χρησιμοποιεί έναν κέρσορα (cursor) για να πλοηγηθεί σ’ όλες τα έγγραφα της συλλογής. Για πιο γρήγορα αποτελέσματα μπορείτε να δημιουργήσετε ευρετήρια (indexes), π.χ.
collection.createIndex(Indexes.text("username"));
System.out.println("Count: " + collection.countDocuments(Filters.text("user admin")));
Μπορείτε να τροποποιήσετε (updateOne()
ή updateMany()
), αντικαταστήσετε (replaceOne()
ή replaceMany()
) ή να διαγράψετε (deleteOne()
ή deleteMany()
) έγγραφα, π.χ.:
collection.updateOne(eq("username", "john"), Updates.set("password", "12345"));
collection.deleteOne(eq("username", "john"));
Τέλος, μπορείτε να εκτελέσετε οποιαδήποτε εντολή MongoDB με την μέθοδο runCommand()
ως εξής:
Document buildInfoResults = database.runCommand(new Document("buildInfo", 1));
System.out.println("Build info: " + buildInfoResults.toJson());
Document collStatsResults = database.runCommand(new Document("collStats", "users"));
System.out.println("Column statistics: " + collStatsResults.toJson());
Μπορείτε να βρείτε τις διαθέσιμες εντολές εδώ.
Περίληψη
Στο μάθημα αυτό μάθαμε πώς να χρησιμοποιούμε βάσεις δεδομένων για ν’ αποθηκεύουμε τα δεδομένα μας. Είδαμε πώς να επικοινωνούμε με σχεσιακές βάσεις δεδομένων μέσω JDBC. Στα μαθήματα της προχωρημένης Java θα δούμε έναν άλλο (πιο αντικειμενοστραφή) τρόπο, το Java Persistence API (JPA). Είδαμε επίσης πώς να επικοινωνούμε με NoSQL βάσεις δεδομένων, δηλ. ΒΔ που δεν ακολουθούν το σχεσιακό μοντέλο.
Στο επόμενο μάθημα, θα δούμε δυο ακόμα τρόπους αναπαράστασης δεδομένων, τα αρχεία XML και τις δομές JSON.
Πηγές
- Date C.J. (1991), An Introduction to Data Base Systems, Vοlume 1, 6th Edition, Addison Wesley.
- Horstmann C. S. (2018), Core Java Volume II - Advanced Features, 11th Ed., Pearson.
- Kreibich J. A. (2010), Using SQLite, O’Reilly.
- O’Donahue J. (2002), Java Database Programming Bible, John Wiley & Sons.
- Reese G. (2001), Database Programming with JDBC and Java, 2nd Ed., O’Reilly.
- Thomas T. M. (2002), Java Data Access—JDBC, JNDI, and JAXP, M&T Books.
- Tyson M. (2019), “What is JDBC? Introduction to the Java Database Connectivity API”, JavaWorld
- Αβούρης Ν. (2001), Βάσεις Δεδομένων και Γνώσεων.
- Κόλλιας Ι. (1991), Βάσεις Δεδομένων, Τόμος 1, Συμμετρία.
- Ξένος Μ. & Χριστοδουλάκης Δ. (2000), Βάσεις Δεδομένων, Τόμος Γ’, ΕΑΠ.
- SQLite Java
- Datatypes In SQLite Version 3
- MongoDB Java Driver Tutorial
<- | Δ | -> |