Hình dung ORM như một người phiên dịch nói chuyện với cơ sở dữ liệu thay bạn. Với hội thoại thường ngày — lưu một đối tượng, tải lại, sửa — họ làm trơn tru. Nhưng khi bạn cần soạn một văn bản kỹ thuật dày đặc, một câu truy vấn báo cáo nhiều tầng, thì bảo người phiên dịch diễn đạt hộ sẽ dài dòng và tam sao thất bản; lúc đó bạn muốn tự viết thẳng bằng ngôn ngữ đích. 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(). JdbcTemplate có queryForObject, 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. Người phiên dịch đọc nguyên văn thứ bạn đọc cho — dán văn bản lạ của người dùng vào câu nói là bị gài ngay.
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 DataSource và cù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:
Đây đúng là cảnh bạn và người phiên dịch cùng sửa một văn bản một lúc — ai lưu sau đè lên người kia. 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.
Muốn tự kiểm trong ba mươi giây, mở 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.
Mẫu số chung
Cái phổ "ORM ↔ trình dựng truy vấn ↔ SQL thuần" có ở mọi hệ sinh thái, chỉ khác tên.
- .NET: EF Core (ORM đầy đủ) ↔ Dapper (micro-ORM, đúng hốc của
JdbcClient) ↔ ADO.NET. - Python: SQLAlchemy ORM ↔ SQLAlchemy Core ↔ DBAPI thô.
- Ruby: ActiveRecord ↔ Arel ↔ SQL thuần.
- Go: cố ý nhẹ —
database/sqlcộngsqlx/sqlc(sqlc sinh mã có kiểu từ SQL, như jOOQ); cộng đồng Go phần lớn né ORM đầy đủ.
Sự thật lặp lại: ORM tối ưu cho 80% ca phổ biến — CRUD trên một cụm đối tượng — và vướng víu đúng ở 20% còn lại: báo cáo, ghi hàng loạt, tính năng riêng CSDL. Đó là "lệch trở kháng đối-tượng–quan-hệ" kinh điển, và tư thế trưởng thành không phải "ORM hay SQL" mà là cả hai, chọn theo từng truy vấn, chung một giao dịch — đúng điều bài này chủ trương.
Và khi rơi xuống SQL thuần, bạn nhận lại hai thứ ORM vốn làm hộ. Một: tham số hoá chống injection — giá trị qua placeholder, còn định danh (tên bảng/cột) không tham số hoá được thì qua danh sách cho phép. Hai: sự nhất quán của bộ nhớ đệm — persistence context không nhìn thấy mũi ghi thô của bạn, y như một bộ đệm có thể lệch với nguồn sự thật. Sợi chỉ chung đáng mang theo: rút SQL thuần ra đúng chỗ trừu tượng của ORM hết trả công (báo cáo, hàng loạt, tính năng gốc), giữ hai công cụ khỏi cùng một hàng, và nhớ rằng mọi tiện nghi ORM đã gỡ đi giờ là việc của bạn phải tự cấp lại.
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.