Chặng dữ liệu khép lại ở đây. Bài này gom những gì đo được thành thứ dùng được.
Bảy con số
21 truy vấn cho thứ cần 1 (bài 24). N+1 trên 20 bản ghi. Trên 5.000 bản ghi thì đó là 5.001 lượt đi về.
join fetch vẫn tốn 2 vì một chữ EAGER (bài 24). @ManyToOne mặc định là EAGER, và đó là chi phí trả ở mọi truy vấn.
Checked exception vẫn được commit (bài 26). Mặc định của Spring chỉ rollback với RuntimeException.
Khoá bi quan cho ra số dư âm 100 (bài 28) — và đó là bằng chứng nó hoạt động. Nếu khoá hỏng, kết quả sẽ là 0, và một trăm đồng biến mất mà trông vẫn hợp lý.
saveAll() nhanh hơn vòng lặp save() đúng 12 ms trên 5000 bản ghi (bài 32) — tức là không. Thứ tạo khác biệt là ba dòng batch_size, và nó nhanh gấp tám.
Tám luồng, pool năm: ba luồng nhận ngoại lệ, không xếp hàng (bài 34). Kích thước pool là trần thông lượng cứng.
H2 chấp nhận ký tự NUL, PostgreSQL từ chối (bài 36). Test xanh, sản xuất đỏ — và blog này đã gặp thật.
Bản đồ quyết định
Cần đọc dữ liệu để hiển thị? Projection hoặc DTO (bài 30). Không lấy entity — nó kéo theo dirty checking, lazy và gấp đôi bộ nhớ.
Danh sách có quan hệ? @EntityGraph (bài 24). Và bật default_batch_fetch_size: 20 từ đầu dự án cho mọi chỗ còn sót.
Bộ lọc do người dùng chọn? Specification (bài 29). Điều kiện trả null thì tự bị bỏ khỏi truy vấn.
Báo cáo, thống kê, hàm cửa sổ? JdbcClient với SQL thuần (bài 31). Đừng ép ORM làm việc của SQL.
Ghi trên 10.000 bản ghi? JdbcTemplate.batchUpdate (bài 32). Dưới đó thì JPA cộng batch_size.
Hai người cùng sửa một bản ghi? @Version trước (bài 28). Chỉ chuyển sang khoá bi quan khi tranh chấp thật sự cao. Và với thao tác tăng giảm có điều kiện, một câu UPDATE ... WHERE thắng cả hai.
Đổi lược đồ? Flyway (bài 33), không phải ddl-auto: update. Và luôn sao lưu trước.
Cần cache? Một bản sao thì Caffeine, nhiều bản sao thì Redis (bài 35, 37). Nhưng trước hết hãy trả lời: đã đo chưa, dữ liệu cũ bao lâu thì chấp nhận được, và ai xoá cache khi dữ liệu đổi từ nơi khác.
Bốn thói quen
Bật log SQL trong lúc phát triển. Một dòng cấu hình, và mọi bài về N+1, batch, truy vấn thừa đều trở thành thứ nhìn thấy được thay vì thứ phải suy đoán.
logging.level.org.hibernate.SQL: DEBUG
Đừng để entity ra khỏi tầng service. Nó giải quyết cùng lúc bốn thứ: LazyInitializationException, N+1 lúc render, vòng lặp tham chiếu khi serialize, và lộ cột nội bộ khi thêm trường mới.
Ba dòng cấu hình đáng đặt vào mọi dự án ngay từ đầu:
spring:
jpa:
open-in-view: false
properties:
hibernate:
jdbc.batch_size: 50
order_inserts: true
default_batch_fetch_size: 20
Không dòng nào có nhược điểm. Cả ba đều là lợi ích chờ tới lúc dữ liệu lớn lên — và lúc đó thì thêm chúng vào sẽ khó hơn nhiều vì mã đã dựa vào hành vi cũ.
Test số truy vấn, đừng chỉ test kết quả. N+1 không làm sai kết quả, nên test chức năng không bao giờ bắt được. Một khẳng định về số câu lệnh JDBC là thứ duy nhất ngăn nó quay lại (bài 36).
Điều rút ra chung
Nhìn lại mười sáu bài, có một mạch xuyên suốt: ORM giấu đi việc truy cập CSDL, và những vấn đề tốn kém nhất đều đến từ chỗ nó giấu quá tốt.
N+1, LazyInitializationException, chèn không gộp lô, cạn pool vì giao dịch quá rộng — cả bốn đều là hệ quả của việc một dòng Java trông vô hại lại sinh ra hoạt động CSDL mà bạn không thấy.
Cách chữa không phải là bỏ ORM. Nó là làm cho phần bị giấu hiện ra: bật log SQL, đếm truy vấn trong test, đọc kế hoạch thực thi khi truy vấn chậm. Sau đó ORM lại rất đáng dùng — nó tiết kiệm thật nhiều mã lặp cho phần vòng đời nghiệp vụ.
Thử ba mươi giây
Bật log SQL, gọi ba endpoint bận nhất của ứng dụng, đếm số câu select cho mỗi cái.
Ba con số đó là bản đồ đầy đủ nhất bạn có về tầng dữ liệu của mình, và nó mất đúng ba mươi giây để có.
Ngày mai bắt đầu chặng bảo mật: Spring Security — kiến trúc chuỗi lọc, và vì sao thêm một phụ thuộc là mọi endpoint bị khoá.