99 Ngày Java — Ngày 97: JDBC
JPA, Spring Data hay jOOQ đều chạy trên JDBC, API chuẩn của Java để nói chuyện với database quan hệ. Hiểu JDBC giúp bạn đọc được stack trace, hiểu transaction thật sự là gì và biết framework đang làm thay mình những việc nào. Bài này dùng JDBC thuần với bốn khái niệm: kết nối, câu lệnh có tham số, transaction và pool.

1. Connection, Statement, ResultSet
Ba đối tượng chính đều giữ tài nguyên ở phía database, nên phải đóng. try-with-resources đóng chúng theo thứ tự ngược lại kể cả khi có ngoại lệ. ResultSet là một con trỏ: next() chuyển sang hàng kế tiếp và trả false khi hết. Cột nên đọc theo tên để code không vỡ khi câu SELECT đổi thứ tự cột. Driver được tìm tự động từ classpath, không cần Class.forName như code rất cũ.
String sql = "SELECT id, total, status FROM orders WHERE customer_id = ?";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, customerId);
try (ResultSet rows = statement.executeQuery()) {
var orders = new ArrayList<Order>();
while (rows.next()) {
orders.add(new Order(rows.getLong("id"),
rows.getBigDecimal("total"), rows.getString("status")));
}
return orders;
}
}2. PreparedStatement và SQL injection
Ghép dữ liệu người dùng vào chuỗi SQL cho phép họ đổi nghĩa câu lệnh: nhập ' OR '1'='1 là lấy được mọi hàng. PreparedStatement gửi câu lệnh và giá trị riêng rẽ; database coi giá trị thuần là dữ liệu, không bao giờ là cú pháp. Đây là cách phòng thủ duy nhất đáng tin, không phải việc tự lọc ký tự. Lưu ý dấu hỏi chỉ thay được giá trị; tên bảng hay tên cột động phải lấy từ một danh sách cho phép trong code.
// vulnerable: user input becomes part of the SQL
String bad = "SELECT * FROM users WHERE email = '" + email + "'";
// safe: the value is bound, never parsed as SQL
var statement = connection.prepareStatement(
"SELECT * FROM users WHERE email = ?");
statement.setString(1, email);3. Transaction thủ công
Mặc định mỗi câu lệnh tự commit. Để nhiều câu lệnh thành một đơn vị hoặc tất cả hoặc không gì cả, tắt auto-commit, chạy các câu lệnh rồi commit; có lỗi thì rollback. Transaction gắn với một Connection, nên mọi câu lệnh trong đó phải dùng cùng một kết nối. Đây chính là điều @Transactional của Spring làm thay bạn: mượn kết nối, tắt auto-commit, commit hoặc rollback, rồi trả kết nối.
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
try {
debit(connection, fromAccount, amount);
credit(connection, toAccount, amount);
connection.commit();
} catch (SQLException | RuntimeException e) {
connection.rollback();
throw e;
}
}4. Connection pool
Mở một kết nối database tốn bắt tay mạng, xác thực và cấp phát bộ nhớ phía server, thường hàng chục mili giây. Pool giữ sẵn một số kết nối để tái sử dụng: getConnection() mượn một cái, close() trả nó về pool chứ không đóng thật. HikariCP là pool mặc định của Spring Boot. Quên đóng kết nối là rò rỉ: pool cạn dần và mọi request sau treo cho tới khi hết thời gian chờ.
Luôn dùng DataSource có pool; DriverManager chỉ hợp cho script nhỏ.
Ghi nhiều hàng thì dùng addBatch và executeBatch.
Đặt query timeout để một truy vấn treo không giữ kết nối mãi.
Bắt SQLException ở ranh giới và chuyển thành ngoại lệ có nghĩa với nghiệp vụ.
var config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/shop");
config.setUsername("shop");
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(10);
DataSource dataSource = new HikariDataSource(config);JDBC là tầng mà mọi công cụ truy cập dữ liệu của Java đứng lên trên; bốn khái niệm hôm nay xuất hiện lại ở mọi framework. Ngày 98 gọi ra thế giới bên ngoài bằng HTTP Client có sẵn trong JDK.
Tài liệu đối chiếu
https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html
https://github.com/brettwooldridge/HikariCP
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


