Bài viết gốc: JDBC - a short guide bởi @MarcoBehler

Cơ bản về Java JDBC trước khi bạn bắt đầu với các thư viện và framework như Hibernate/JPA. Nắm rõ các kiến thức cơ bản về JDBC giúp tự tin khi làm việc với Hibernate/JPA.

Để có thể thực hành bạn cần MySQL hoặc PostgreSQL server, cách đơn giản nhất là triển khai bằng Docker.

Tải về sourcecode kèm theo docker-compose.yml tại đây https://github.com/truongvantuan/jdbc-learning

What is JDBC?

JDBC giúp bạn kết nối ứng dụng Java với Hệ quản trị cơ sở giữ liệu (DBMS). Không quan trọng là loại ứng dụng nào, server-side hay frontend GUI. Và cũng không quan trọng loại Database nào, có thể là MySQL, PostgreSQL, SQLite,...

Cách tốt nhất để mọi ứng dụng Java kết nói với database là thông qua JDBC API.

Hơn nữa, bạn không cần phải cài đặt JDBC API như một thư viện của bên thứ 3 bởi vì mặc định JDBC đi cùng với JDK/JRE.

Điều duy nhất bạn cần là bắt đầu với JDBC là trình điều khiển (driver) cụ thể cho từng database.

JDBC Driver

Trình điều khiển JDBC là gì?

Để kêt nối tới database bằng Java, bạn cần một thứ gọi là trình điều kiển JDBC (JDBC Driver). Mọi database ( MySQL, Oracle, PostgreSQL) đều có JDBC Driver của riêng nó, thường được viết bởi nhà phát triển Database đó và dễ tìm thấy trên website của database.

Driver thực hiện một lượng lớn công việc quan trọng trong quá trình kết nối đến database của bạn. Cơ bản như mở kết nối socket từ ứng dụng Java tới database, gửi các truy vấn SQL, tới các tính năng nâng cao như cung cấp khả năng nhận các sự kiện từ database (Oracle).

Ví dụ MySQL database, để kết nối từ Java bạn cần đi đến MySQL Website, tải về MySQL JDBC Driver dưới định dạng .jar (được gọi là Connector/J) và thêm nó vào dự án của bạn.

Tìm trình điều khiển mới nhất cho Database ở đâu?

Danh sách driver của các database phổ biến nhất.

Nếu bạn quản lý dự án với Maven?

Bạn dễ dàng thêm JDBC driver như là phụ thuộc (dependency) vào dự án thay vì thêm file .jar theo cách phổ thông.

Bạn sẽ tìm thấy mọi JDBC Driver trong danh sách sau được liệt kê bởi bác Vlad MIhalcea:

https://vladmihalcea.com/jdbc-driver-maven-dependency/

Trong trường hợp này là MySQL, bạn cần thêm tag sau vào file pom.xml

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

Hướng dẫn JDBC

Kết nối tới database như thế nào?

Tạo MySQL server bằng cách tại thư mục gốc của sourcecode chạy lệnh:

docker compose up -d

Ngay sau khi bạn thêm driver vào dự án, ứng dụng Java của bạn đã sẵn sàng mở các kết nối đến database.

Chúng ta đã thêm trình điều khiển mysql-connector-java, do đó sẽ kết nối tới MySQL database. Nếu bạn từng sử dụng database trước đó, điều này giống với việc bạn mở ứng dụng terninal và khởi chạy lệnh mysql để sử dụng giao diện dòng lệnh (command-line interface) của MySQL.

Để kết nối tới MySQL database có tên test (được khởi tạo cùng với docker-compose.yml ở trên), đang chạy trên máy của bạn với username/password hợp lệ, bạn sẽ khởi chạy code sau.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class TestMysqlConnect {

    String url = "jdbc:mysql://localhost:3306/test";
    String user = "root";
    String password = "password";

    public Connection connect() {
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(url, user, password);
            System.out.println("Kết nối thành công đến MySQL Database");
        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }
        return conn;
    }

    public static void main(String[] args) {
        TestMysqlConnect app = new TestMysqlConnect();
        app.connect();
    }

}

Giờ hãy xem kỹ hơn đoạn code này:

Connection conn = null;
try {
    conn = DriverManager.getConnection(url, user, password);
    System.out.println("Kết nối thành công đến MySQL Database");
} catch (SQLException e) {
    System.out.println(e.getMessage());
}

Cách đơn giản nhất để mở một kết nối database là gọi hàm DriverManager.getConnection, nó sẽ tự động phát hiện JDBC driver bạn đã thêm vào dự án.

Ta có 3 tham số:

  • [0] URL: Một JDBC URL hợp lệ. Bạn sẽ biết thêm nó ở phần sau.

  • [1] Username: tên đăng nhập MySQL.

  • [2] Password: mật khẩu.

    Khi đã có kết nối, bạn có thế thực hiện các truy vấn SQL. Ví dụ trên đơn giản là kiểm tra kết nối thành công hay không. Có một điều cần lưu ý:

try {
    conn = DriverManager.getConnection(url, user, password);
    System.out.println("Kết nối thành công đến MySQL Database");
} catch (...) {...}

Để ý getConnection đặt trong khối try-catch, đó là câu lệnh try-with-resource và nó đảm bảo rằng kết nối của bạn tự động đóng khi kết thúc khối try-catch , bạn không cần phải đóng nó thủ công. Đây là một điểm rất hay của try-with-resource.

Sau khi đã hoàn thành việc kết nối, bạn có thể sẵn sàng cho các truy vấn vào database.

Hiểu thêm về cấu trúc URL trong JDBC Connection

String truyền vào hàm DriverManager.getConnection() ở trên được gọi là chuỗi kết nối JDBC (URL). Chi tiết hơn sẽ là:

  • Luôn bắt đầu với jdbc, theo sau đó là dấu (:).
  • Tiếp đến là định danh của database, ví dụ mysql, oracle, theo sao đó là dấu (:).
  • Kế tiếp là địa chỉ nơi database server đang chạy.

Cùng xem lại ví dụ với MySQL ở trên:

.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=UTC",user,password);
  • Bắt đầu với jdbc.
  • Tiếp đến là mysql.
  • Cuối cùng là địa chỉ MySQL server, cùng các thuộc tính nếu có ?serverTimezone=UTC

Dưới đây là ví dụ JDBC URL cho từng database cụ thể:

  • MySQL → jdbc:mysql://localhost/mydb
  • Postgres → dbc:postgresql://localhost/test
  • Oracle → jdbc:oracle:thin:@prodHost:1521:ORCLE
  • SQL Server → jdbc:sqlserver://localhost;instance=SQLEXPRESS;databaseName=myDb

Tham khảo danh sách đầy đủ các kết nối JDBC trình bày bởi bác Vlad Mihalcea  tại JDBC Driver Strings.

Thực thi câu lệnh SQL SELECT

String sql = "SELECT * FROM users WHERE first_name = ?";
        try {
            Connection conn = this.connect();
            PreparedStatement selectStatement = conn.prepareStatement(sql);
            selectStatement.setString(1, name);
            ResultSet rs = selectStatement.executeQuery();
            while (rs.next()) {
                String firstName = rs.getString("first_name");
                String lastName = rs.getString("last_name");
                System.out.println("firstName = " + firstName + "," + "lastName= " + lastName);
            }

        } catch (SQLException e) {
            System.out.println(e.getMessage());
        }

Có 2 phần quan trọng cho truy vấn SELECT ở trên. Thứ nhất,

PreparedStatement selectStatement = conn.prepareStatement(sql);
selectStatement.setString(1, name);

Đầu tiên bạn sẽ cần tạo PreparedStatement, cái sẽ nhận sql query như là đầu vào cùng với các giữ chỗ (placeholder) được đánh dấu ?. Ở đây ta đã giữ 1 chổ để sau đó truyền vào name tại hàm setString(1, name)

Bạn có thể dử dụng placeholder (?) cho bất kì loại dữ liệu nào được an toàn khởi việc xóa sạch database từ SQL Inhection. Bạn sẽ cần đặt chúng tại thời điểm dùng PreparedStatement với setString(), setInt(). Và cũng lưu ý rằng, placeholders là số, bắt đầu với 1. Do đó nếu bạn có 2 chỗ (?) trong câu lệnh SQL, bạn sẽ là như sau setString(2, "string")

Thứ hai, việc thực hiện truy vấn sẽ trả về cho bạn ResultSet. Đó là danh sách tất cả các hàng mà database tìm thấy cho một truy vấn nhất định. Bạn sẽ phải duyệt qua kết quả bằng vòng lặp while, cụ thể là while (rs.next()) {...}

while (rs.next()) {
    String firstName = rs.getString("first_name");
    String lastName = rs.getString("last_name");
    System.out.println("firstName = " + firstName + "," + "lastName= " + lastName );
}

Trong ví dụ trên, chúng ta đơn giản in ra first_namelast_name của mọi user tìm thấy. Lưu ý rằng, ResultSet cung cấp các hàm get khác nhau, như là getString, getInt, getDate. Bạn cần sử dụng đúng với loại dữ liệu của các cột trong bảng. first_name, last_name là các cột có kiểu VARCHAR như khai báo ở trên nên ta sử dụng getString.

Thực thi câu lệnh SQL INSERT

void insertIntoTable(String first_name, String last_name) {
        String sql = "INSERT INTO users (first_name, last_name) VALUES (?,?)";
        try {
            Connection conn = this.connect();
            PreparedStatement statement = conn.prepareStatement(sql);
            statement.setString(1, first_name);
            statement.setString(2, last_name);
            int insertedRows = statement.executeUpdate();
            System.out.println("Đã chèn vào bảng " + insertedRows + " user");
        } catch (SQLException e) {
            System.out.printf(e.getMessage());
        }
    }

Hãy xem xét kỹ hơn các câu lệnh sau:

String sql = "INSERT INTO users (first_name, last_name) VALUES (?,?)";
...
PreparedStatement statement = conn.prepareStatement(sql);
    statement.setString(1, first_name);
    statement.setString(2, last_name);

Câu lệnh INSERT hàng vào bảng cũng giống với SELECT hàng từ bảng. Ta lại tạo ra một PreparedStatement và kèm với nó là gọi hàm executeUpdate trên nó (lưu ý tên của hàm, không có hàm executeInsert).

Một lần nữa, bạn sử dụng PreparedStatement với giữ chỗ ? (placeholder).

int insertedRows = statement.executeUpdate();
System.out.println("I just inserted " + insertedRows + " users")

Bạn không phải duyệt qua kết quả ResultSet như truy vấn SELECT, thay vào đó executeUpdate sẽ trả về số hàng đã được chèn vào bảng (ví dụ trên trả về 1 vì ta chỉ chèn vào 1 user mà thôi).

Thực thi câu lệnh SQL UPDATE

Connection conn = this.connect();
PreparedStatement updateStatement = conn.prepareStatement(sql);
updateStatement.setInt(1, user_id);
int updatedRows = updateStatement.executeUpdate();
System.out.println("Đã cập nhật " + updatedRows + " user");

Cập nhật hàng về cơ bản giống với việc chèn hàng. Bạn tạo một PreparedStatement và gọi hàm executeUpdate.

Một lần nữa, bạn nên sử dụng PreparedStatement với giữ chỗ ? (placeholder) để bảo vệ chống lại việc đưa vào SQL. ExecuteUpdate bây giờ sẽ trả về cho bạn số lượng hàng được cập nhật thực tế.

Thực thi câu lệnh SQL DELETE

Truy vấn SQL Deletes hoàn toàn giống với SQL Updates.

PreparedStatement deleteStatement = connection.prepareStatement("DELETE FROM users WHERE id > ?");
deleteStatement.setInt(1, 1);
int deletedRows = deleteStatement.executeUpdate();
System.out.println("Đã xóa " + deletedRows + " users");

JDBC Connection Pooling

JDBC Connection Pool là gì?

Mở và đóng kết nối tới database như ta đã làm ở trên bằng cách dùng DriverManager.getConnection rất tốn thời gian, bị lặp lại nhiều lần.

Đặt biệt với ứng dụng Web, bạn thật sự không muốn cứ mỗi lần người dùng tương tác, thực hiện truy vấn SQL lại phải mở mới một kết nối tới database. Bạn sẽ thích việc có một nhóm các kết nối luôn được mở và chia sẽ giữa các người dùng. Và đây là điều mà JDBC Connnection Pool làm.

Connection Pool mở một số lượng nhỏ kết nối tới database và thay vì tự mình mở các kết nối, bạn sẽ yêu cầu Connection Pool cung cấp cho bạn một trong các kết nối đang mở đó.

JDBC Connection Pool nào tốt nhất?

Chúng ta có các ứng cử viên sau:

Apache Commons DBCP / C3P0 đã cũ, thiếu các cấu hình mặc định, hiệu năng thấp, gặp vấn đề trong nắm bắt các lỗi.

Vì thế các công nghệ mới được tin dùng hơn. HikariCP or Vibur-dbcp là những lựa chọn tuyệt vời. Sử dụng Oracle’s UCP nếu bạn làm việc với Oracle Databases.

Tất các các Connection Pool đó đều tin cậy, hiệu năng cao, cung cấp cấu hình mặc định cũng như khả năng nắm bắt các lỗi.

Sử dụng JDBC Connection Pool như thế nào?

Tùy thuộc vào tùy chọn bạn chọn, bạn sẽ không cần mở các kết nối  thủ công qua DriverManager. Thay vào đó bạn sẽ xây dựng một Connection Pool, thông qua Interface DataSource và yêu cầu nó cung cấp cho bạn các kết nối tới database.

package org.example;

import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.sql.*;

public class TestHikariConnect {

    private static String url = "jdbc:mysql://localhost:3306/test";
    private static String user = "root";
    private static String password = "password";

    public static void main(String[] args) throws SQLException {

        // Mở duy nhất một kết nối tới database thông qua Hikari Connection Pool
        Connection conn = createdHikariDS().getConnection();

        // thực hiện truy vấn SELECT thông qua kết nối cung cấp từ Hikari Connection Pool
        String selectQuery = "select * from users";
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(selectQuery);
        while (rs.next()) {
            String firstName = rs.getString("first_name");
            String lastName = rs.getString("last_name");
            System.out.println("firstName = " + firstName + "," + "lastName= " + lastName);
        }

        // Tiếp tực truy vấn INSERT thông qua Hikari Connection Pool ở trên
        String insertInto = "INSERT INTO users (first_name, last_name) VALUES (?,?)";
        PreparedStatement insert = conn.prepareStatement(insertInto);
        insert.setString(1, "Bao Chau");
        insert.setString(2, "Ngo");
        int insertedRows = insert.executeUpdate();
        System.out.println("\nĐã thêm vào " + insertedRows + " hàng");
    }

    // Tạo DataSource thay thế cho DriverManager từ các thông tin đăng nhập ở trên
    static DataSource createdHikariDS() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        return dataSource;
    }
}

Đoạn code này:

static DataSource createdHikariDS() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        return dataSource;
    }

Tương đương với:

Connection connect() throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }

Với Hikari bạn chỉ cần tạo một kết nối duy nhất:

// Mở duy nhất một kết nối tới database thông qua Hikari Connection Pool
Connection conn = createdHikariDS().getConnection();

Dùng nó cho các truy vấn tới database mà không cần tạo mới connection.

Như bạn thấy, bạn không mở trực tiếp các kết nối thông qua DriverManager nữa mà thay vào đó bạn yêu cầu DataSource. Lần đầu tiên khi chúng ta dùng HikariDataSource kết nối cơ sở dữ liệu, nó sẽ khởi tạo một nhóm kết nối mở - mặc định gồm 10 kết nối cơ sở dữ liệu và cung cấp cho bạn một trong 10 kết nối này.

Bài viết này trình bày những kiến ​​thức cơ bản về JDBC, từ driver đến xử lý kết nối và truy vấn SQL, đến tổng hợp kết nối (connection pool).

Tóm lại, khi sử dụng JDBC, bạn đang làm việc tầng gần nhất với SQL. Bạn có đầy đủ sức mạnh và tốc độ của SQL và JDBC trong tầm tay, nhưng JDBC không đi kèm với các tính năng giúp bạn lập trình hiệu quả, nhanh chóng (hãy nghĩ về công việc thủ công khi duyệt qua ResultSet là như thế nào).

Do đó các thư viện và database framework cho Java khác xuất hiện. Từ các JDBC wrappers nhẹ cho đến các giải pháp ORM toàn diện như Hibernate / JPA.