Apache Derby Bài 16: Thực thi truy vấn với Statement và ResultSet
1. Vai trò của Statement và ResultSet trong JDBC
Sau khi đối tượng Connection được thiết lập thành công, chúng ta cần một công cụ để mang các câu lệnh SQL từ Java chuyển đến Apache Derby Engine để thực thi. Trong JDBC, nhiệm vụ này do đối tượng Statement đảm nhận.
-
Statement: Đóng vai trò là một "phương tiện vận chuyển". Nó nhận chuỗi SQL thô, gửi đến cơ sở dữ liệu và nhận lại phản hồi. -
ResultSet: Đóng vai trò là "thùng chứa kết quả". Khi bạn gửi một câu lệnhSELECT, cơ sở dữ liệu sẽ trả về một bảng dữ liệu.ResultSetgiúp bạn duyệt qua từng dòng của bảng dữ liệu này trong bộ nhớ Java.
2. Hai phương thức thực thi cốt lõi của Statement
Đối tượng Statement cung cấp nhiều phương thức, nhưng có hai phương thức được sử dụng thường xuyên nhất, phân tách rõ ràng giữa hành động thay đổi dữ liệu và hành động đọc dữ liệu:
1. executeUpdate(String sql)
-
Sử dụng cho: Các lệnh DDL (
CREATE,DROP,ALTER) và DML (INSERT,UPDATE,DELETE). -
Giá trị trả về: Trả về một số nguyên (
int) đại diện cho số dòng (row count) bị ảnh hưởng bởi câu lệnh. Nếu chạy lệnh DDL (nhưCREATE TABLE), nó thường trả về0.
2. executeQuery(String sql)
-
Sử dụng cho: Duy nhất lệnh đọc dữ liệu DQL (
SELECT). -
Giá trị trả về: Trả về một đối tượng
ResultSetchứa danh sách các bản ghi truy vấn được. Không bao giờ trả vềnull.
3. Duyệt dữ liệu với ResultSet
ResultSet duy trì một con trỏ (cursor) trỏ tới dòng hiện tại của tập kết quả.
Ban đầu, con trỏ nằm ở vị trí trước dòng đầu tiên (before first row).
Quy trình đọc dữ liệu chuẩn:
-
Gọi phương thức
rs.next()để di chuyển con trỏ xuống dòng tiếp theo. Phương thức này trả vềtruenếu có dữ liệu, hoặcfalsenếu đã đến cuối tập kết quả. -
Sử dụng vòng lặp
while (rs.next())để duyệt qua toàn bộ bảng. -
Tại mỗi dòng, dùng các hàm
get...()(nhưgetInt,getString,getDouble) để lấy giá trị của từng cột. Có thể truyền vào tên cột (dễ đọc) hoặc chỉ số cột (bắt đầu từ 1, chạy nhanh hơn một chút).
4. Thực hành: Tích hợp hoàn chỉnh mã nguồn thao tác Derby
Đoạn mã dưới đây minh họa toàn bộ vòng đời: Tạo bảng Chèn dữ liệu Truy vấn và in ra Console. Chúng ta tiếp tục sử dụng cú pháp try-with-resources lồng nhau để đảm bảo Statement và ResultSet được đóng sạch sẽ nhằm tránh thất thoát tài nguyên (resource leak).
Java
package com.hieu.derby;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DerbyStatementDemo {
private static final String JDBC_URL = "jdbc:derby:my_embedded_db;create=true";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(JDBC_URL);
Statement stmt = conn.createStatement()) {
System.out.println("1. Khởi tạo đối tượng Statement thành công.");
// Khối 1: Thực thi lệnh DDL - Tạo bảng
// Lưu ý: Bọc trong try-catch phụ để tránh crash nếu bảng đã tồn tại ở lần chạy thứ 2
try {
String createTableSql = "CREATE TABLE nhan_vien ("
+ "id INT PRIMARY KEY, "
+ "ho_ten VARCHAR(100), "
+ "luong DECIMAL(10,2))";
stmt.executeUpdate(createTableSql);
System.out.println("=> Tạo bảng 'nhan_vien' thành công.");
} catch (SQLException e) {
// Mã lỗi X0Y32 của Derby có nghĩa là "Bảng đã tồn tại"
if ("X0Y32".equals(e.getSQLState())) {
System.out.println("=> Bảng 'nhan_vien' đã tồn tại, bỏ qua bước tạo.");
} else {
throw e; // Ném ra các lỗi khác
}
}
// Khối 2: Thực thi lệnh DML - Chèn dữ liệu
String insertSql = "INSERT INTO nhan_vien (id, ho_ten, luong) VALUES "
+ "(1, 'Nguyen Minh', 25000000.00), "
+ "(2, 'Tran Hoang', 18000000.00)";
int rowsInserted = stmt.executeUpdate(insertSql);
System.out.println("2. Đã chèn thành công " + rowsInserted + " dòng dữ liệu.");
// Khối 3: Thực thi lệnh DQL - Truy vấn dữ liệu
System.out.println("3. Đọc dữ liệu từ cơ sở dữ liệu:");
String selectSql = "SELECT id, ho_ten, luong FROM nhan_vien ORDER BY luong DESC";
// ResultSet cũng cần được đóng sau khi sử dụng
try (ResultSet rs = stmt.executeQuery(selectSql)) {
System.out.println("-------------------------------------------------");
System.out.printf("%-5s | %-20s | %-15s\n", "ID", "HỌ TÊN", "MỨC LƯƠNG");
System.out.println("-------------------------------------------------");
while (rs.next()) {
// Trích xuất dữ liệu theo tên cột
int id = rs.getInt("id");
String hoTen = rs.getString("ho_ten");
double luong = rs.getDouble("luong");
System.out.printf("%-5d | %-20s | %-15.2f\n", id, hoTen, luong);
}
System.out.println("-------------------------------------------------");
}
} catch (SQLException e) {
System.err.println("Lỗi thao tác CSDL: " + e.getMessage());
}
}
}
5. Hạn chế chí mạng của Statement truyền thống
Cách làm dùng toán tử cộng chuỗi (String concatenation) trong Statement ở trên chỉ phù hợp cho các câu lệnh SQL tĩnh (hardcoded). Trong ứng dụng thực tế, dữ liệu cần chèn được nhập từ form người dùng. Việc nối chuỗi tạo ra hai vấn đề lớn:
-
Lỗi cú pháp nội tại: Nếu tên người dùng nhập vào có chứa dấu nháy đơn (ví dụ:
O'Brian), truy vấn SQL sẽ bị gãy cú pháp do sai cặp ngoặc nháy. -
Lỗ hổng bảo mật nghiêm trọng (SQL Injection): Người dùng có thể cố tình nhập vào các chuỗi mã độc như
' OR 1=1; DROP TABLE nhan_vien; --để thao túng và phá hủy toàn bộ cơ sở dữ liệu của bạn. -
Hiệu năng kém: Mỗi lần bạn gọi
Statement, Derby Engine phải dịch lại (parse) và tối ưu hóa (compile) câu lệnh SQL từ đầu, gây lãng phí CPU nếu câu lệnh đó được lặp lại hàng nghìn lần.
Để giải quyết triệt để vấn đề này, JDBC cung cấp một phiên bản nâng cấp mạnh mẽ hơn.
All rights reserved