JDBC bài 14 Xử lý các kiểu dữ liệu thời gian: Date, Time, Timestamp và LocalDate/LocalDateTime
Xử lý thời gian (Date/Time) luôn là một trong những chủ đề đau đầu nhất trong lập trình Backend do sự sai lệch về múi giờ (Timezone) giữa Server Java và Server Database, cộng thêm sự thay đổi lịch sử của chính ngôn ngữ Java.
Trong JDBC, việc xử lý thời gian được chia làm hai kỷ nguyên rõ rệt: Kỷ nguyên cũ (java.sql.*) và Kỷ nguyên hiện đại (Java 8 Time API).
1. Kỷ nguyên cũ: java.sql.Date, Time và Timestamp
Trước Java 8, JDBC cung cấp 3 class nằm trong package java.sql để ánh xạ trực tiếp với các kiểu dữ liệu thời gian trong cơ sở dữ liệu:
java.sql.Date: Chỉ lưu trữ Ngày - Tháng - Năm (Tương ứng kiểu DATE trong SQL).java.sql.Time: Chỉ lưu trữ Giờ : Phút : Giây (Tương ứng kiểu TIME trong SQL).java.sql.Timestamp: Lưu trữ đầy đủ cả Ngày, Giờ và có độ chính xác đến mili-giây (Tương ứng kiểu DATETIME hoặc TIMESTAMP trong SQL).
Cách sử dụng cũ:
// Lấy thời gian hiện tại
java.sql.Timestamp now = new java.sql.Timestamp(System.currentTimeMillis());
pstmt.setTimestamp(1, now);
// Đọc dữ liệu
java.sql.Timestamp createdAt = rs.getTimestamp("created_at");
Tại sao chúng bị "hắt hủi" trong các dự án hiện đại?
Cả 3 class này đều kế thừa từ lớp java.util.Date khét tiếng của Java. Chúng có chung những nhược điểm chí mạng:
- Mutable (Có thể thay đổi): Bạn có thể gọi
date.setTime()để thay đổi giá trị của nó sau khi khởi tạo, gây ra rủi ro rất lớn về tính nhất quán dữ liệu trong môi trường đa luồng (multi-threading). - Không an toàn về múi giờ: Chúng phụ thuộc quá nhiều vào múi giờ mặc định của hệ điều hành đang chạy ứng dụng.
2. Kỷ nguyên hiện đại: Sức mạnh của java.time (Java 8+)
Từ Java 8, Oracle đã giới thiệu một bộ API thời gian hoàn toàn mới (package java.time) vô cùng mạnh mẽ, an toàn (Immutable) và rõ ràng. Ba class tương ứng thường được sử dụng ở tầng Backend là:
LocalDate: Lưu Ngày.LocalTime: Lưu Giờ.LocalDateTime: Lưu Ngày và Giờ.
Tin vui cho các lập trình viên JDBC: Từ phiên bản JDBC 4.2 (đi kèm với Java 8), bạn không cần phải chuyển đổi (convert) qua lại giữa
java.time.*vàjava.sql.*nữa. JDBC Driver hiện đại hỗ trợ làm việc trực tiếp với các class mới này.
3. Cách ánh xạ Java 8 Time API trong JDBC
Bởi vì PreparedStatement và ResultSet được thiết kế từ trước khi Java 8 ra đời, chúng không có sẵn các hàm như setLocalDate() hay getLocalDateTime().
Thay vào đó, chúng ta sẽ sử dụng phương thức đa năng setObject() và getObject() (truyền kèm theo Class type) để xử lý.
Bảng ánh xạ chuẩn JDBC 4.2:
| Kiểu dữ liệu SQL | Class Java 8 (java.time) |
Cách Ghi (PreparedStatement) |
Cách Đọc (ResultSet) |
|---|---|---|---|
| DATE | LocalDate |
pstmt.setObject(1, localDate) |
rs.getObject("col", LocalDate.class) |
| TIME | LocalTime |
pstmt.setObject(1, localTime) |
rs.getObject("col", LocalTime.class) |
| DATETIME / TIMESTAMP | LocalDateTime |
pstmt.setObject(1, localDateTime) |
rs.getObject("col", LocalDateTime.class) |
4. Thực hành: Insert và Read thời gian chuẩn xác
Dưới đây là đoạn mã áp dụng tiêu chuẩn mới nhất để lưu trữ một hóa đơn thanh toán cùng thời điểm giao dịch vào cơ sở dữ liệu.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class ModernDateTimeExample {
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) {
// 1. CHÈN DỮ LIỆU (INSERT)
String insertSQL = "INSERT INTO invoices (amount, invoice_date, created_at) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
pstmt.setDouble(1, 1500000.0);
// Sử dụng LocalDate cho cột DATE (Ví dụ: Ngày phát hành hóa đơn)
LocalDate invoiceDate = LocalDate.now();
pstmt.setObject(2, invoiceDate);
// Sử dụng LocalDateTime cho cột DATETIME/TIMESTAMP (Ví dụ: Thời điểm hệ thống ghi nhận)
LocalDateTime createdAt = LocalDateTime.now();
pstmt.setObject(3, createdAt);
pstmt.executeUpdate();
System.out.println("Đã lưu hóa đơn vào lúc: " + createdAt);
} catch (SQLException e) {
e.printStackTrace();
}
// 2. ĐỌC DỮ LIỆU (READ)
String selectSQL = "SELECT id, invoice_date, created_at FROM invoices ORDER BY id DESC LIMIT 1";
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(selectSQL);
ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
// Trích xuất trực tiếp ra class của Java 8 bằng getObject()
LocalDate readInvoiceDate = rs.getObject("invoice_date", LocalDate.class);
LocalDateTime readCreatedAt = rs.getObject("created_at", LocalDateTime.class);
System.out.println("--- Dữ liệu vừa đọc từ DB ---");
System.out.println("Ngày hóa đơn: " + readInvoiceDate);
System.out.println("Thời điểm tạo: " + readCreatedAt);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Ghi chú quan trọng dành cho hệ thống phân tán: Nếu Hiếu đang thiết kế một hệ thống global (người dùng ở nhiều quốc gia), hãy cân nhắc cấu hình Database luôn chạy ở múi giờ UTC, hoặc sử dụng kiểu dữ liệu
TIMESTAMP WITH TIME ZONE(phổ biến trong PostgreSQL) kết hợp với classOffsetDateTimetrong Java để đảm bảo mọi mốc thời gian đều chuẩn xác tuyệt đối trên toàn cầu.
All rights reserved