0

Apache Derby Bài 29: Sử dụng Sequence và cột tự tăng (IDENTITY / GENERATED ALWAYS AS IDENTITY)

1. Vấn đề của việc tự sinh khóa chính (Primary Key)

Trong thiết kế cơ sở dữ liệu, chúng ta thường sử dụng các con số vô nghĩa (Surrogate Key) làm Khóa chính, ví dụ như id = 1, 2, 3....

Nhiều lập trình viên mới vào nghề thường tự sinh ID bằng cách dùng lệnh SELECT MAX(id) + 1 FROM ten_bang trước khi thực hiện INSERT. Tuy nhiên, trong môi trường đa kết nối (Network Server) mà chúng ta vừa học ở phần trước, cách làm này sẽ gây ra lỗi Race Condition (Tương tranh). Nếu hai client cùng gọi MAX(id) tại cùng một phần nghìn giây, cả hai sẽ nhận được cùng một số (ví dụ: 100), và khi INSERT sẽ có một người bị lỗi trùng khóa chính (Duplicate Key).

Để giải quyết triệt để, Apache Derby cung cấp cơ chế sinh số tự động ở cấp độ Engine, đảm bảo tính nguyên tử (atomic) tuyệt đối: Cột IDENTITY và đối tượng SEQUENCE.

2. Phương pháp 1: Cột tự tăng (IDENTITY Column)

Đây là cách phổ biến và dễ nhất. Bạn gắn thuộc tính tự tăng trực tiếp vào định nghĩa của một cột (thường là khóa chính) khi tạo bảng.

Apache Derby hỗ trợ hai biến thể của Identity:

Biến thể 1: GENERATED ALWAYS AS IDENTITY

Hệ thống nắm quyền kiểm soát 100%. Bạn không được phép truyền giá trị thủ công vào cột này khi INSERT. Nếu bạn cố tình truyền, Derby sẽ báo lỗi. Đây là cách được khuyến nghị mạnh mẽ nhất để đảm bảo tính toàn vẹn.

SQL

CREATE TABLE khach_hang (
    ma_khach INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),
    ten_khach VARCHAR(100) NOT NULL
);

  • START WITH 1: Bắt đầu đếm từ số 1.

  • INCREMENT BY 1: Mỗi lần có dòng mới, ID tăng thêm 1.

Cách chèn dữ liệu: Bạn bắt buộc phải bỏ qua cột ma_khach trong danh sách INSERT:

SQL

INSERT INTO khach_hang (ten_khach) VALUES ('Nguyen Van A');
-- Derby sẽ tự động gán ma_khach = 1

Biến thể 2: GENERATED BY DEFAULT AS IDENTITY

Hệ thống sẽ tự sinh số nếu bạn không truyền. Tuy nhiên, nếu bạn chủ động truyền một con số vào, Derby vẫn chấp nhận.

SQL

CREATE TABLE san_pham (
    ma_sp INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    ten_sp VARCHAR(100)
);

Biến thể này thường chỉ dùng khi bạn đang chuyển đổi dữ liệu (Migration) từ một hệ thống cũ sang Derby và muốn giữ nguyên các ID cũ, sau đó mới để Derby tự tăng tiếp. Nếu dùng không cẩn thận, nó sẽ gây ra xung đột ID trong tương lai.

3. Phương pháp 2: Đối tượng SEQUENCE (Chuỗi sinh số độc lập)

Từ phiên bản 10.6, Derby hỗ trợ chuẩn SQL SEQUENCE. Khác với cột IDENTITY bị gắn chặt vào một bảng cụ thể, SEQUENCE là một đối tượng độc lập trong cơ sở dữ liệu.

Khi nào nên dùng SEQUENCE? Khi bạn có nhiều bảng khác nhau nhưng muốn chúng dùng chung một dải ID duy nhất (ví dụ: bảng don_hang_b2b và don_hang_b2c không được phép có mã đơn hàng trùng nhau).

Cú pháp tạo Sequence:

SQL

CREATE SEQUENCE seq_ma_don_hang
    AS INT
    START WITH 1000
    INCREMENT BY 1;

Cách sử dụng Sequence: Để lấy số tiếp theo từ chuỗi này, bạn dùng hàm NEXT VALUE FOR.

SQL

-- Chèn vào bảng B2B
INSERT INTO don_hang_b2b (ma_don, khach_hang) 
VALUES (NEXT VALUE FOR seq_ma_don_hang, 'Cong ty ABC'); 
-- ma_don sẽ là 1000

-- Chèn vào bảng B2C
INSERT INTO don_hang_b2c (ma_don, khach_hang) 
VALUES (NEXT VALUE FOR seq_ma_don_hang, 'Anh Hieu'); 
-- ma_don sẽ là 1001

4. Tích hợp Java: Lấy lại ID vừa được tự động sinh ra

Đây là câu hỏi kinh điển nhất khi làm việc với JDBC: "Tôi vừa INSERT một khách hàng mới, làm sao tôi biết Derby đã sinh ra ID số mấy để tôi có thể dùng nó chèn tiếp vào bảng địa_chỉ?"

Tuyệt đối không dùng SELECT MAX(id) để lấy lại ID, vì một luồng khác có thể vừa chèn thêm dữ liệu ngay sau bạn. Thay vào đó, JDBC cung cấp cờ Statement.RETURN_GENERATED_KEYS.

Dưới đây là đoạn mã Java chuẩn mực để giải quyết bài toán này:

Java

String sql = "INSERT INTO khach_hang (ten_khach) VALUES (?)";

// 1. Thêm cờ RETURN_GENERATED_KEYS khi chuẩn bị lệnh
try (PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
    
    pstmt.setString(1, "Khach hang VIP");
    int affectedRows = pstmt.executeUpdate();
    
    if (affectedRows > 0) {
        // 2. Trích xuất ResultSet chứa các khóa vừa được sinh ra
        try (ResultSet rs = pstmt.getGeneratedKeys()) {
            if (rs.next()) {
                int idMoiSinh = rs.getInt(1); // Lấy giá trị của cột đầu tiên
                System.out.println("=> Khách hàng mới có ID là: " + idMoiSinh);
                
                // Bây giờ bạn có thể dùng idMoiSinh để INSERT vào các bảng con khác...
            }
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}

5. Tổng kết

  • Để hệ quản trị CSDL tự sinh ID là cách duy nhất đảm bảo an toàn dữ liệu trong môi trường đa kết nối.

  • GENERATED ALWAYS AS IDENTITY: Dùng cho 90% trường hợp khi khóa chính chỉ thuộc về riêng một bảng.

  • SEQUENCE: Dùng khi cần một bộ đếm đếm chung cho nhiều bảng, hoặc cần sinh số trước khi thực hiện INSERT.

  • Trong Java, luôn dùng Statement.RETURN_GENERATED_KEYS kết hợp với getGeneratedKeys() để truy xuất chính xác ID vừa được tạo ra bởi chính PreparedStatement của bạn.

Bài tiếp theo sẽ đưa chúng ta đến với một khái niệm tối quan trọng để đảm bảo tính toàn vẹn dữ liệu khi thực hiện nhiều thao tác INSERT, UPDATE, DELETE liên tiếp: Apache Derby Bài 30: Quản lý Giao dịch (Transaction) và tính chất ACID (Commit, Rollback).


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí