Skip to the content.

5.4 Επικοινωνία με Βάσεις Δεδομένων

© Γιάννης Κωστάρας


<- Δ ->

Στα προηγούμενα μαθήματα είδαμε πώς μπορούμε ν’ αποθηκεύουμε μόνιμα τα δεδομένα μας σε συστήματα αρχείων. Σ’ αυτό το μάθημα θα μάθουμε πώς ν’ αποθηκεύουμε τα δεδομένα μας σε βάσεις δεδομένων.

Κατηγορίες Βάσεων Δεδομένων

Υπάρχουν διάφορες κατηγορίες βάσεων δεδομένων:

Σχεσιακές Βάσεις Δεδομένων

Το πιο παραδοσιακό μοντέλο είναι οι σχεσιακές βάσεις δεδομένων (Σχεσιακά Συστήματα Διαχείρισης Βάσεων Δεδομένων - ΣΣΔΒΔ ή Relational Database Management Systems - RDBMS). Σ’ αυτές, τα δεδομένα αποθηκεύονται σε πίνακες οι οποίοι σχετίζονται μεταξύ τους. Η επικοινωνία μ’ αυτές γίνεται με τη 4ης γενιάς γλώσσα Structured Query Language (SQL). Οι εντολές της SQL χωρίζονται στις εξής δυο κατηγορίες:

Τα πιο δημοφιλή ΣΣΔΒΔ είναι τα ακόλουθα:

Η 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 και πώς μπορούμε να επικοινωνήσουμε μ’ αυτή.

  1. Κάντε κλικ στο μενού Window –> Services για να εμφανίσετε το παράθυρο Services
  2. Εμφανίστε τον κόμβο Databases και κάντε δεξί κλικ στον κόμβο Drivers και επιλέξτε New Driver
  3. Στο διαλογικό παράθυρο New JDBC Driver που εμφανίζεται, πατήστε το κουμπί Add… και επιλέξτε το αρχείο sqlite-jdbc-3.xx.xx.jar που κατεβάσατε. Ως όνομα δώστε SQLite3
  4. Κάντε δεξί κλικ στον κόμβο Databases και επιλέξτε New Connection. Στο διαλογικό παράθυρο που εμφανίζεται, επιλέξτε τον οδηγό SQLite3 που δημιουργήσατε στο προηγούμενο βήμα και πατήστε Next.
  5. Προαιρετικά δώστε ένα User Name και Password για να συνδεθείτε στη ΒΔ, και ως JDBC URL δώστε τη διαδρομή που θέλετε ν’ αποθηκεύσετε τη ΒΔ (ή τη διαδρομή της ΒΔ που δημιουργήσαμε προηγούμενα): jdbc:sqlite:<path>/UserDB.sqlite3
  6. Πατήστε Finish

Το NetBeans δημιούργησε μια νέα σύνδεση (Connection) στη ΒΔ που δημιουργήσατε που περιέχει 3 κόμβους: Tables, Views, Procedures. Μπορείτε να δημιουργήσετε πίνακες, να δείτε τα δεδομένα τους, να εισάγετε νέα δεδομένα κλπ.

Εικόνα 5.4.4 Πίνακας Users στο NetBeans

Στη συνέχεια θα δημιουργήσουμε ένα πρόγραμμα Java για να επικοινωνήσουμε με τη ΒΔ UserDB.sqlite3.

  1. Στο NetBeans, δημιουργήστε ένα νέο έργο Java Application και δώστε ένα όνομα στην εφαρμογή, π.χ. UserDBApp
  2. Επιλέξτε τα Use Dedicated Folder for Storing Libraries και Create Main Class, αφήστε τα προτεινόμενα ονόματα και πατήστε το Finish
  3. Δεξί κλικ στο Libraries και Add JAR/Folder. Επιλέξτε το sqlite-jdbc-3.xx.xx.jar και Copy to Libraries Folder και στη συνέχεια το κουμπί Choose
  4. Στη 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? truefalse) αν ο χρήστης 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 στη ΒΔ.

Μπορείτε ακόμα να χρησιμοποιήσετε CallableStatements για κλήση Αποθηκευμένων Διαδικασιών (Stored Procedures) και Save Points αλλά ξεφεύγουν από το σκοπό του εισαγωγικού αυτού μαθήματος.

Μη σχεσιακές ΒΔ

Με την παραγωγή πολλών δεδομένων, εμφανίστηκαν οι περιορισμοί των σχεσιακών ΒΔ. Με τον ορισμό “Υπέρογκα Δεδομένα (Big Data)” εννοούμε μεγάλες ποσότητες δεδομένων που αλλάζουν συχνά και δεν έχουν την ίδια δομή ή μπορεί να είναι αδόμητα (π.χ. μετεωρολογικά/σεισμικά δεδομένα, πωλήσεις κλπ.). Δουλεύοντας με υπέρογκα δεδομένα σημαίνει να μπορούμε να:

Οι ΒΔ που μπορούν ν’ αποθηκεύουν τέτοιου είδους δεδομένα ονομάζονται NoSQL. Συνήθως δεν έχουν κάποιο schema (αδόμητα δεδομένα) όπως οι σχεσιακές ΒΔ. Οι NoSQL ΒΔ χωρίζονται στις παρακάτω κατηγορίες (λίστα με NoSQL ΒΔ):

Επικοινωνία με 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):

  1. Εξ’ ορισμού ο διακομιστής MongoDB (mongod) δημιουργεί τη ΒΔ στην τοποθεσία (/data/db). Στα Windows, ενημερώστε το κλειδί dbpath στο αρχείο mongo.config με τη διαδρομή που θέλετε να αποθηκεύετε τη ΒΔ. Στο Unix/Linux, εκκινήστε το διακομιστή ως εξής: ./mongod --dbpath <path> όπου θα πρέπει να έχετε ήδη δημιουργήσει το φάκελο, π.χ. ../db (άρα θα αποθηκευθεί στο mongodb-x.x.x/db). Ο διακομιστής εξυπηρετεί στη θύρα 27017.
  2. Στο NetBeans, κάντε δεξί κλικ στο MongoDB στην καρτέλα Services και επιλέξτε New Connection. Δώστε ένα όνομα, π.χ. TestMongoDB, πατήστε το κουμπί ... και δώστε ένα όνομα στη ΒΔ (π.χ. usersdb) και προαιρετικά Username, Password για περισσότερη ασφάλεια και OK.
  3. Δεξί κλικ στον κόμβο TestMongoDB και Connect.
  4. Δεξί κλικ στον κόμβο usersdb και Add Collection. Δώστε ως Collection name users. Μια ΒΔ τύπου εγγράφων (document) περιέχει συλλογές.
  5. Δεξί κλικ στον κόμβο users και Open.
  6. Πατήστε το κουμπί 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.

Πηγές

  1. Date C.J. (1991), An Introduction to Data Base Systems, Vοlume 1, 6th Edition, Addison Wesley.
  2. Horstmann C. S. (2018), Core Java Volume II - Advanced Features, 11th Ed., Pearson.
  3. Kreibich J. A. (2010), Using SQLite, O’Reilly.
  4. O’Donahue J. (2002), Java Database Programming Bible, John Wiley & Sons.
  5. Reese G. (2001), Database Programming with JDBC and Java, 2nd Ed., O’Reilly.
  6. Thomas T. M. (2002), Java Data Access—JDBC, JNDI, and JAXP, M&T Books.
  7. Tyson M. (2019), “What is JDBC? Introduction to the Java Database Connectivity API”, JavaWorld
  8. Αβούρης Ν. (2001), Βάσεις Δεδομένων και Γνώσεων.
  9. Κόλλιας Ι. (1991), Βάσεις Δεδομένων, Τόμος 1, Συμμετρία.
  10. Ξένος Μ. & Χριστοδουλάκης Δ. (2000), Βάσεις Δεδομένων, Τόμος Γ’, ΕΑΠ.
  11. SQLite Java
  12. Datatypes In SQLite Version 3
  13. MongoDB Java Driver Tutorial

<- Δ ->