Chín bài vừa qua nói về JPA. Bài này nói về lúc nên bỏ nó.

Bốn chỗ JPA không hợp

Báo cáo và thống kê. Gộp nhóm nhiều bảng, hàm cửa sổ, CTE đệ quy — viết bằng Criteria thì dài gấp ba và khó đọc hơn hẳn, mà kết quả không phải entity nên chẳng được lợi ích gì của ORM.

Ghi hàng loạt. Bài 32 đo được: JdbcTemplate.batchUpdate chèn 5000 bản ghi trong 29 ms, nhanh hơn JPA đã tối ưu ba tới bốn lần.

Truy vấn cần đúng một kế hoạch thực thi cụ thể. Khi bạn đã đọc EXPLAIN và biết chính xác câu SQL mình muốn, để ORM sinh lại nó là tự làm khó.

Tính năng riêng của CSDL. JSONB của PostgreSQL, tìm kiếm toàn văn, UPSERT với ON CONFLICT, kiểu mảng. Có cách nhồi vào JPA, nhưng chúng đều xấu hơn viết thẳng.

JdbcClient

Spring 6.1 thêm JdbcClient — API fluent thay cho JdbcTemplate:

record TomTat(String ten, long soBai) {}

jdbcClient.sql("""
        select t.ten, count(b.id) as so_bai
        from tac_gia t left join bai b on b.tac_gia_id = t.id
        group by t.ten order by so_bai desc, t.ten limit 3
        """)
    .query((rs, i) -> new TomTat(rs.getString("ten"), rs.getLong("so_bai")))
    .list();
  TomTat[ten=Tac gia 0, soBai=5]
  TomTat[ten=Tac gia 1, soBai=5]
  TomTat[ten=Tac gia 10, soBai=5]

Có sẵn khi bạn đã có spring-boot-starter-data-jpa, không cần thêm phụ thuộc.

So với JdbcTemplate, ba cải thiện thật:

Tham số đặt tên:

jdbcClient.sql("select * from don where trang_thai = :tt and ngay > :ngay")
    .param("tt", "MOI")
    .param("ngay", tuNgay)
    .query(Don.class).list();

JdbcTemplate dùng ? theo thứ tự, và đảo nhầm hai tham số cùng kiểu là lỗi không ai phát hiện.

Ánh xạ tự động sang record:

.query(TomTat.class).list()

Khớp tên cột với tên thành phần, so_bai sang soBai tự động.

Một API cho mọi kiểu trả về.list(), .single(), .optional(), .set(). JdbcTemplatequeryForObject, queryForList, queryForMap, query với đủ kiểu tham số.

Với dự án dùng Spring 6.1 trở lên, tôi dùng JdbcClient cho mọi chỗ SQL mới.

Ba việc nữa nó làm

Ghi và lấy số dòng:

int soDong = jdbcClient.sql("update don set trang_thai = :tt where id = :id")
        .param("tt", "HUY").param("id", id).update();

Lấy khoá vừa sinh:

var kh = new GeneratedKeyHolder();
jdbcClient.sql("insert into don(ma) values (:ma)").param("ma", ma).update(kh);
Long id = kh.getKeyAs(Long.class);

Tham số danh sách — chỗ JdbcTemplate khó chịu nhất:

.param("ids", List.of(1, 2, 3))        // tự bung thành IN (?, ?, ?)

Vẫn phải dùng tham số

JdbcClient không tự bảo vệ bạn khỏi SQL injection nếu bạn ghép chuỗi:

jdbcClient.sql("select * from don where ma = '" + ma + "'")     // LỖ HỔNG

Bài 65 sê-ri Java đã cho thấy một chuỗi 28 ký tự xoá được cả bảng, không ngoại lệ nào được ném ra.

Mọi giá trị đi qua :tenThamSo. Tên bảng và tên cột không thay bằng tham số được — phải đối chiếu với danh sách cho phép, đúng như phần sắp xếp ở bài 29.

Sống chung với JPA

Hai công cụ dùng cùng một DataSourcecùng một giao dịch. Gọi jdbcClient bên trong một phương thức @Transactional là nó tham gia giao dịch đó — không cần cấu hình gì.

Nhưng có một cái bẫy thật:

JDBC không biết gì về persistence context. Sửa một dòng bằng SQL trong khi Hibernate đang giữ entity tương ứng, và entity đó vẫn mang giá trị cũ. Lúc commit, dirty checking có thể ghi đè lại thay đổi của bạn.

Hai cách tránh: gọi em.flush() trước khi chạy SQL và em.clear() sau, hoặc — tốt hơn — tách hẳn: bảng nào do JPA quản thì đừng ghi bằng JDBC.

Ranh giới tôi dùng: JDBC cho đọc (báo cáo, thống kê) và cho ghi hàng loạt vào bảng riêng; JPA cho vòng đời nghiệp vụ.

Vài lựa chọn khác

jOOQ — sinh mã Java từ lược đồ CSDL, nên SQL của bạn được kiểm kiểu lúc biên dịch. Đổi tên cột là lỗi biên dịch, không phải lỗi lúc chạy. Rất tốt nếu SQL là trung tâm của dự án; cái giá là một bước sinh mã và giấy phép thương mại với CSDL không mã nguồn mở.

MyBatis — SQL nằm trong XML hoặc chú thích, ánh xạ tường minh. Phổ biến ở nhiều đội, đặc biệt là những đội có DBA viết SQL riêng.

Spring Data JDBC — đơn giản hơn JPA nhiều: không lazy, không dirty checking, không persistence context. Bạn lưu và tải nguyên cụm đối tượng. Nếu mô hình của bạn hợp với nó, phần lớn bài học từ bài 24 tới 27 không còn áp dụng — vì các cơ chế gây ra chúng không tồn tại.

Thử ba mươi giây

Mở một phương thức repository phức tạp nhất của bạn — cái có @Query dài nhất hoặc nhiều Specification nhất — rồi thử viết nó bằng SQL thuần.

Nếu bản SQL ngắn hơn và dễ đọc hơn, đó là câu trả lời. ORM có ích ở chỗ nó có ích; ép nó làm việc của SQL thì bạn trả giá bằng cả hai.

Ngày mai: chèn hàng loạt — và một dòng cấu hình làm JPA nhanh gấp tám.