0

JDBC bài 8 Kỹ thuật Create: Thêm dữ liệu mới và lấy lại ID tự tăng (Generated Keys)

Trong các hệ thống quản trị cơ sở dữ liệu như MySQL hay PostgreSQL, khóa chính (Primary Key) thường được thiết lập chế độ tự động tăng (AUTO_INCREMENT hoặc SERIAL). Khi thực hiện lệnh INSERT, bạn không truyền ID vào câu SQL, mà Database sẽ tự động sinh ra ID này.

Vấn đề đặt ra là: Làm sao để ứng dụng Java ngay lập tức lấy lại được cái ID vừa sinh ra đó? (Thao tác này cực kỳ phổ biến khi bạn cần tạo một bản ghi cha, lấy ID của nó, rồi tiếp tục insert các bản ghi con).

1. Hạn chế của lệnh executeUpdate() thông thường

Nếu bạn chỉ dùng lệnh stmt.executeUpdate(insertSQL) như ở Bài 6, kết quả trả về duy nhất là một số nguyên (ví dụ: 1) đại diện cho số dòng được thêm thành công. Hoàn toàn không có thông tin về ID mới.

Nhiều lập trình viên mới thường chữa cháy bằng cách viết thêm một câu lệnh thứ hai: SELECT MAX(id) FROM users hoặc SELECT LAST_INSERT_ID(). Tuy nhiên, cách này chứa đựng rủi ro Race Condition cực kỳ nguy hiểm trong môi trường đa luồng (Multi-threading). Khi hàng ngàn request cùng insert một lúc, bạn rất dễ lấy nhầm ID của một người dùng khác vừa được tạo cùng mili-giây đó.

2. Giải pháp chuẩn xác: Statement.RETURN_GENERATED_KEYS

JDBC giải quyết triệt để bài toán này bằng cách cung cấp một "cờ" (flag) yêu cầu Driver mang ID về cùng với kết quả insert. Bạn chỉ cần thay đổi cách gọi hàm executeUpdate và sử dụng thêm phương thức getGeneratedKeys().

Quy trình 2 bước:

  1. Khai báo cờ khi thực thi: stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);

  2. Lấy ResultSet chứa ID: ResultSet rs = stmt.getGeneratedKeys();

3. Thực hành: Insert và lấy ID tự tăng ngay lập tức

Dưới đây là đoạn mã hoàn chỉnh minh họa cách thêm một đơn hàng mới và lấy lại mã đơn hàng (Order ID) để sử dụng cho logic tiếp theo.

Java

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

public class GeneratedKeysExample {
    private static final String URL = "jdbc:mysql://localhost:3306/shop_db";
    private static final String USER = "root";
    private static final String PASSWORD = "password123";

    public static void main(String[] args) {
        String insertSQL = "INSERT INTO orders (customer_name, total_amount) VALUES ('Nguyen Minh Hieu', 1500000)";

        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             Statement stmt = conn.createStatement()) {

            // 1. Thực thi lệnh INSERT và truyền cờ yêu cầu trả về khóa tự sinh
            int affectedRows = stmt.executeUpdate(insertSQL, Statement.RETURN_GENERATED_KEYS);
            
            if (affectedRows > 0) {
                System.out.println("Tạo đơn hàng thành công!");

                // 2. Lấy ResultSet chứa ID vừa được tạo
                try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
                    
                    // Vì chỉ insert 1 dòng nên dùng if (generatedKeys.next()) thay vì while
                    if (generatedKeys.next()) {
                        // Thông thường ID tự sinh nằm ở cột đầu tiên (index 1) của ResultSet này
                        long newOrderId = generatedKeys.getLong(1);
                        System.out.println("ID đơn hàng vừa tạo là: " + newOrderId);
                        
                        // Bạn có thể dùng newOrderId này để insert tiếp vào bảng order_details
                    } else {
                        System.out.println("Thêm thành công nhưng không lấy được ID tự tăng.");
                    }
                }
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

4. Đặc điểm của getGeneratedKeys()

  • Trả về ResultSet: Dù chỉ là một con số ID, JDBC vẫn nhất quán đóng gói nó vào trong đối tượng ResultSet. Do đó, bạn bắt buộc phải gọi next() để con trỏ đi vào dòng dữ liệu đầu tiên trước khi dùng getLong(1) hoặc getInt(1).

  • An toàn tuyệt đối (Thread-safe): Cơ chế này do chính Driver và hệ quản trị Database phối hợp xử lý ở tầng connection session. Nó đảm bảo 100% ID trả về thuộc về chính câu lệnh INSERT bạn vừa thực thi, không bao giờ bị nhầm lẫn với các request của người dùng khác.

  • Hoạt động với Batch: Nếu bạn insert nhiều dòng cùng lúc, ResultSet này sẽ chứa một danh sách nhiều ID tương ứng với từng dòng được thêm vào (Sẽ tìm hiểu sâu hơn ở phần Batch Processing).


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.