0

Apache Derby Bài 39: Xử lý dữ liệu thời gian (DATE, TIME, TIMESTAMP) và bài toán Timezone trong Derby

1. Ba kiểu dữ liệu thời gian cốt lõi

Apache Derby tuân thủ chặt chẽ tiêu chuẩn SQL cho việc lưu trữ dữ liệu thời gian thông qua 3 kiểu dữ liệu riêng biệt. Việc chọn đúng kiểu dữ liệu không chỉ giúp tiết kiệm không gian lưu trữ mà còn làm tăng tính rõ ràng cho thiết kế CSDL.

  1. DATE (Ngày): Lưu trữ ngày, tháng, năm. Bỏ qua hoàn toàn giờ phút giây.

    • Định dạng chuẩn: YYYY-MM-DD (Ví dụ: 2026-10-10)

    • Ứng dụng: Ngày sinh, ngày hết hạn thẻ, ngày lễ.

  2. TIME (Giờ): Chỉ lưu trữ giờ, phút, giây. Bỏ qua ngày tháng.

    • Định dạng chuẩn: HH:MM:SS (Ví dụ: 10:59:08)

    • Ứng dụng: Cấu hình giờ báo thức, giờ bắt đầu ca làm việc.

  3. TIMESTAMP (Dấu thời gian): Kết hợp cả DATE và TIME, đồng thời hỗ trợ độ chính xác lên đến nano-giây (thường là 6 đến 9 chữ số thập phân).

    • Định dạng chuẩn: YYYY-MM-DD HH:MM:SS.NNNNNN

    • Ứng dụng: Ghi log hệ thống (created_at, updated_at), thời điểm thanh toán thành công.

Cú pháp tạo bảng:

SQL

CREATE TABLE phien_giao_dich (
    ma_phien INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    ngay_giao_dich DATE,
    gio_khop_lenh TIME,
    thoi_diem_hoan_tat TIMESTAMP
);

2. Các hàm thời gian nội tại (Built-in Functions)

Trong các lệnh INSERT hoặc UPDATE, thay vì phải gửi chuỗi thời gian từ Java xuống, bạn có thể yêu cầu Derby tự động lấy thời gian hiện tại của máy chủ bằng các hàm nội tại:

  • CURRENT_DATE (hoặc CURRENT DATE)

  • CURRENT_TIME (hoặc CURRENT TIME)

  • CURRENT_TIMESTAMP (hoặc CURRENT TIMESTAMP)

SQL

-- Cập nhật thời điểm sửa đổi mới nhất bằng hàm của Derby Engine
UPDATE don_hang 
SET trang_thai = 'DA_XU_LY', updated_at = CURRENT_TIMESTAMP 
WHERE id = 105;

3. Nút thắt cổ chai: Bài toán Múi giờ (Timezone)

Khác với PostgreSQL (hỗ trợ kiểu TIMESTAMP WITH TIME ZONE), Apache Derby KHÔNG lưu trữ thông tin múi giờ kèm theo dữ liệu thời gian. Khi bạn lưu chuỗi 2026-10-10 10:59:00 vào cột TIMESTAMP, Derby chỉ lưu đúng các con số đó dưới dạng Local Time (Thời gian cục bộ).

Điều này dẫn đến một sự cố kinh điển trong các hệ thống phân tán:

  1. Máy chủ CSDL Derby đặt tại Nhật Bản (Múi giờ GMT+9).

  2. Server Web Java đặt tại Việt Nam (Múi giờ GMT+7).

  3. Server Web gọi hàm CURRENT_TIMESTAMP của Derby, kết quả ghi vào CSDL là 12:59 (Giờ Nhật Bản). Nhưng khi người dùng tại Việt Nam đọc lên, ứng dụng lại không biết đó là giờ Nhật, nên hiển thị thẳng 12:59, khiến dữ liệu bị sai lệch 2 tiếng.

4. Best Practice: Nguyên tắc "Lưu trữ UTC - Hiển thị Local"

Để giải quyết triệt để rắc rối về Timezone, mọi hệ thống Backend hiện đại đều phải tuân thủ một nguyên tắc duy nhất: Toàn bộ dữ liệu TIMESTAMP trong cơ sở dữ liệu phải được ép về chuẩn UTC (GMT+0). Múi giờ (như Asia/Saigon hay America/New_York) chỉ được áp dụng ở tầng Application (Java/Frontend) khi cần hiển thị cho người dùng.

Để làm được điều này, chúng ta cần phối hợp cấu hình JVM và sử dụng API thời gian hiện đại của Java 8 (java.time).

Bước 1: Ép JVM của máy chủ chạy ở múi giờ UTC

Khi khởi động ứng dụng Java (hoặc Derby Network Server), hãy thêm cờ (flag) này vào lệnh khởi động:

Bash

java -Duser.timezone=UTC -jar my-application.jar

Lúc này, mọi hàm CURRENT_TIMESTAMP của Derby sẽ tự động trả về giờ UTC chuẩn quốc tế.

Bước 2: Tương tác qua JDBC với java.time.*

Trước JDBC 4.2, lập trình viên phải dùng lớp java.sql.Timestamp kỹ thuật số rất rườm rà. Hiện nay, bạn nên dùng trực tiếp LocalDateTime (chứa ngày giờ) hoặc ZonedDateTime của Java 8 và đẩy qua hàm setObject().

Mã nguồn chèn và đọc thời gian bằng Java:

Java

package com.hieu.derby.datetime;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class DerbyTimezoneDemo {

    public static void main(String[] args) {
        String url = "jdbc:derby://localhost:1527/log_db;create=true";
        
        try (Connection conn = DriverManager.getConnection(url)) {
            
            // 1. Tạo dữ liệu thời gian chuẩn UTC tại tầng Java
            ZonedDateTime nowInVietnam = ZonedDateTime.now(ZoneId.of("Asia/Saigon"));
            // Ép về UTC trước khi ghi xuống DB
            LocalDateTime utcTimeToSave = nowInVietnam.withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime();

            // 2. Ghi xuống Derby bằng setObject()
            String insertSql = "INSERT INTO nhat_ky_he_thong (hanh_dong, thoi_gian) VALUES (?, ?)";
            try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
                pstmt.setString(1, "USER_LOGIN");
                pstmt.setObject(2, utcTimeToSave); // JDBC 4.2 tự động map LocalDateTime vào TIMESTAMP
                pstmt.executeUpdate();
                System.out.println("=> Đã lưu sự kiện vào CSDL với mốc thời gian UTC: " + utcTimeToSave);
            }

            // 3. Đọc từ Derby và chuyển ngược về múi giờ Việt Nam
            String selectSql = "SELECT hanh_dong, thoi_gian FROM nhat_ky_he_thong";
            try (PreparedStatement pstmt = conn.prepareStatement(selectSql);
                 ResultSet rs = pstmt.executeQuery()) {
                 
                if (rs.next()) {
                    // Đọc nguyên bản từ DB (hiểu ngầm là UTC do quy ước thiết kế)
                    LocalDateTime dbTime = rs.getObject("thoi_gian", LocalDateTime.class);
                    
                    // Gắn múi giờ UTC vào thời gian vừa đọc
                    ZonedDateTime utcZonedTime = dbTime.atZone(ZoneId.of("UTC"));
                    
                    // Chuyển đổi sang múi giờ hiển thị cho khách hàng (Việt Nam)
                    ZonedDateTime vietnamDisplayTime = utcZonedTime.withZoneSameInstant(ZoneId.of("Asia/Saigon"));
                    
                    System.out.println("=> Thời gian hiển thị cho user (GMT+7): " + vietnamDisplayTime);
                }
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5. Các hàm thao tác thời gian nội tại

Nếu bạn cần tính toán logic thời gian ngay trong câu truy vấn SQL (Ví dụ: Tìm các đơn hàng được tạo trong 7 ngày qua), Derby cung cấp các hàm chuyên dụng:

  • YEAR(date) / MONTH(date) / DAY(date) / HOUR(time): Trích xuất một thành phần cụ thể.

  • Toán tử khoảng thời gian (Interval): Derby không có hàm DATEADD hay DATEDIFF chuẩn mực như SQL Server, nhưng bạn có thể dùng cú pháp {fn TIMESTAMPADD(...)} của chuẩn JDBC Escape syntax.

Ví dụ: Lấy các đơn hàng đã tạo quá 30 ngày.

SQL

SELECT * FROM don_hang 
WHERE thoi_diem_hoan_tat < {fn TIMESTAMPADD(SQL_TSI_DAY, -30, CURRENT_TIMESTAMP)};

(Cú pháp {fn ...} sẽ được JDBC Driver của Derby tự động biên dịch thành mã nội bộ tương ứng trước khi gửi xuống Engine).

Như vậy, để hệ thống hoạt động ổn định và nhất quán về mặt dữ liệu không gian - thời gian, bạn cần kỷ luật áp dụng chuẩn UTC tại tầng CSDL. Ở bài tiếp theo, chúng ta sẽ quay trở lại với một kỹ thuật tối ưu hóa hiệu năng cực kỳ quan trọng đã được nhắc đến ở Bài 26: Apache Derby Bài 40: Quản lý Connection Pool với thư viện HikariCP.


All Rights Reserved

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