Truy vấn JPA nâng cao
Bộ mở rộng Spring Data JPA: truy vấn động bằng Specification, base repository/service, annotation điều kiện trên DTO, JOIN/SELECT động, ánh xạ kết quả query về VO và JPA auditing tự động. Dùng khi service thao tác dữ liệu quan hệ qua JPA mà không muốn viết tay Criteria/Specification cho từng form tìm kiếm.
Khi nào sử dụng
- Dựng truy vấn động có nhiều điều kiện tùy chọn (form tìm kiếm) mà không phải viết tay
Criteria/Specification. - Dùng base repository với thao tác batch, upsert chỉ cập nhật trường non-null và truy vấn theo
Specification. - Cần JPQL/native query trả thẳng về VO/DTO mà không cần interface projection hay constructor expression.
- Muốn tự động điền
createdBy,createdAt,updatedBy,updatedAtcho entity. - Cần cách ly điều kiện tìm kiếm trong DTO bằng annotation thay vì viết logic if/else.
- Cần JOIN động không phụ thuộc quan hệ
@ManyToOne/@OneToMany, chọn cột động, subqueryEXISTShoặc so sánh cột-với-cột.
Cài đặt
<dependency>
<groupId>vn.govex.cloud</groupId>
<artifactId>govex-cloud-data-jpa</artifactId>
</dependency>
Version quản lý qua BOM/parent — xem Cài đặt. Khi dùng govex-cloud-parent, khai báo <relativePath/> để Maven luôn resolve parent từ Maven repository thay vì tìm trong thư mục cục bộ.
Module kéo sẵn các dependency cần thiết, ứng dụng không phải khai báo thêm:
| Dependency | Vai trò |
|---|---|
govex-cloud-data-common | Path, Paths, SFunc, TypedField, @Limit — bộ mô tả trường và biểu thức dùng chung |
govex-cloud-core | Tiện ích nền tảng (String/Collection/Json/lambda...) |
spring-boot-starter-data-jpa | Spring Data JPA + Hibernate |
jsqlparser, guava | Parser SQL cho cơ chế viết lại câu lệnh native |
Yêu cầu JDK 17 trở lên (khuyến nghị JDK 21) và Spring Boot 3.x theo BOM của workspace.
Cấu hình
| Property | Mô tả | Mặc định |
|---|---|---|
govex.jpa.auditing.enabled | Bật JPA Auditing để tự động điền thông tin audit cho entity; chỉ tắt khi đặt false | true |
Tắt JPA auditing:
govex:
jpa:
auditing:
enabled: false
Dependency ghi đè Hibernate property hibernate.session_factory.statement_inspector để viết lại câu lệnh native phục vụ SQLExpression. Khi ứng dụng đã tự cấu hình statement inspector, khai báo bean tên govexJpaHibernatePropertiesCustomizer để tự quyết định property này.
Auto-configuration JpaAutoConfiguration đăng ký sẵn các thành phần:
AuditorAwarelấy người thao tác từSecurityContext(chỉ khi cógovex-cloud-securitytrên classpath).RewriteStatementInspectorviết lại câu lệnh SQL theoSQLExpression.MapToProjectionConverterchuyểnMap<String, Object>sang class implementsProjection.TableMetadataquét metadata entity/column khiEntityManagerFactorykhởi tạo.QuerySingleResultAspectxử lý kết quả đơn cho@QuerySingleResult.- Cấu hình
@EnableJpaAuditingkhigovex.jpa.auditing.enabledkhácfalse.
Bật repository mở rộng
Dùng @EnableExtendJpaRepositories thay cho @EnableJpaRepositories để repository dùng base class và query lookup của Govex:
@Configuration
@EnableExtendJpaRepositories(basePackages = "vn.govex.cloud.demo")
public class JpaConfig {
}
Annotation này là alias của @EnableJpaRepositories với các default:
repositoryFactoryBeanClass = ExtendJpaRepositoryFactoryBeanrepositoryBaseClass = SimpleBaseJpaRepository
Nhờ đó mọi repository được tạo sẽ hỗ trợ base repository mở rộng, projection converter và các tiện ích truy vấn của module.
Base repository
public interface OrderRepository extends BaseRepository<Order, String> {
}
BaseRepository mở rộng JpaRepository + JpaSpecificationExecutor và bổ sung:
| Method | Mô tả |
|---|---|
getOne(spec) / getOne(spec, sort) | Trả về entity đầu tiên thỏa điều kiện; ném NoResultException nếu không có kết quả |
findOne(spec) / findOne(spec, sort) | Như trên nhưng trả Optional |
findAll(spec) / findAll(spec, sort) | Danh sách entity thỏa điều kiện |
findAll(spec, pageable) | Trang dữ liệu (Page) theo điều kiện, phân trang và sắp xếp |
findAll(spec, limit) / findAll(spec, sort, limit) | Giới hạn số bản ghi bằng Limit của Spring Data |
saveBatch(entities) / saveBatch(entities, batchSize) | Thêm mới theo lô (mặc định 1000), flush từng lô; chỉ dùng cho insert |
updateBatch(entities) / updateBatch(entities, batchSize) | Cập nhật theo lô bằng merge; chỉ dùng cho update |
saveOrUpdateByNotNullProperties(entity) | Upsert một entity: kiểm tra tồn tại rồi chỉ ghi các trường non-null |
saveOrUpdateAllByNotNullProperties(entities) | Upsert theo lô, kiểm tra tồn tại từng bản ghi |
deleteByIds(ids) | Xóa từng ID (loop deleteById), hiệu năng thấp |
deleteBatchByIds(ids) / deleteBatchByIds(ids, batchSize) | Xóa theo lô bằng điều kiện in, mặc định 1000 |
Các overload nhận SQLExpression (truy vấn native động) được trình bày trong Native query & SQLExpression.
Ví dụ thao tác batch/upsert:
orderRepository.saveBatch(orders); // insert theo lô 1000
orderRepository.updateBatch(orders); // update theo lô 1000
orderRepository.saveOrUpdateAllByNotNullProperties(orders); // upsert, chỉ ghi field non-null
orderRepository.deleteBatchByIds(List.of("1", "2")); // xóa theo lô bằng IN
Base service
IService<T, ID> cung cấp sẵn các thao tác CRUD/batch và ủy quyền xuống BaseRepository: save, saveAll, saveBatch, deleteById, delete(Specification), findById, findAll, findAll(spec), findAll(pageable), findAll(spec, pageable), count, existsById, getOne(spec)...
ServiceImpl<R, T, ID> là lớp triển khai abstract, tự inject @Autowired R repository (kiểu BaseRepository) và tự resolve entityClass từ generic type:
public interface OrderRepository extends BaseRepository<Order, String> {
}
@Service
@RequiredArgsConstructor
public class OrderService extends ServiceImpl<OrderRepository, Order, String> {
private final OrderMapper mapper;
public void importOrders(List<Order> orders) {
saveBatch(orders);
}
public Page<OrderVO> search(OrderSearchDTO request, Pageable pageable) {
TypedJPASpecification<Order> spec = Specifications.ofBeanTyped(request)
.equal(Order::getDeleted, false);
return repository.findAll(spec, pageable)
.map(mapper::toVO);
}
}
Entity base class & converter
Module cung cấp sẵn các lớp cơ sở cho entity:
| Class | Mô tả |
|---|---|
IdEntity | Khóa chính id kiểu String sinh tự động bằng UUID |
BaseEntity | IdEntity + thông tin audit (createdBy, createdAt, updatedBy, updatedAt) |
AbstractEntity | Chỉ implements DataEntity, không có field nào |
AbstractAuditEntity | Có thông tin audit nhưng không có khóa chính |
Các AttributeConverter dùng chung:
SoftDeleteConverter:Boolean↔Integer(1/0) cho cờ xóa mềm.JsonSetConverter:Set<String>↔ chuỗi JSON (không phụ thuộc kiểu cột JSON của từng DB).- Ngoài ra còn
JsonListConverter,JsonMapConverter,JsonObjectConverter,StringListConverter,StringSetConverter.
Truy vấn động với Specification
Khởi tạo
import vn.govex.cloud.data.jpa.specification.Specifications;
// Điều kiện theo tên cột dạng chuỗi
JPASpecification<Order> spec = Specifications.where();
// Điều kiện theo method reference (type-safe)
TypedJPASpecification<Order> typedSpec = Specifications.whereTyped();
TypedJPASpecification<Order> typedSpecWithClass = Specifications.whereTyped(Order.class);
// Dựng từ DTO có annotation điều kiện
JPASpecification<Order> beanSpec = Specifications.ofBean(searchDTO);
TypedJPASpecification<Order> typedBeanSpec = Specifications.ofBeanTyped(searchDTO);
| API | Trả về | Dùng khi |
|---|---|---|
Specifications.where() | JPASpecification<T> | Tham chiếu trường bằng tên cột dạng chuỗi ("status", "customer.name") |
Specifications.whereTyped() | TypedJPASpecification<T> | Bắt buộc cho method reference (Order::getStatus) |
Specifications.whereTyped(Class<T>) | TypedJPASpecification<T> | Như trên, kèm kiểu entity đích |
Specifications.ofBean(bean) | JPASpecification<T> | Dựng từ DTO có annotation điều kiện |
Specifications.ofBean(bean, paths) | JPASpecification<T> | Như trên, override alias/entityAlias/ignore bằng Paths |
Specifications.ofBeanTyped(bean) | TypedJPASpecification<T> | Dựng từ DTO annotation theo hướng type-safe |
Specifications.ofBeanTyped(bean, paths) | TypedJPASpecification<T> | Kết hợp DTO annotation với cấu hình Paths |
Chuyển đổi giữa hai dạng:
TypedJPASpecification<Order> typedSpec = Specifications.where()
.equal("status", "ACTIVE")
.like("customer.name", keyword)
.typed();
Tự động bỏ qua giá trị rỗng
Khi không truyền tham số match, phương thức tự kiểm tra giá trị và chỉ thêm điều kiện khi giá trị khác null và không rỗng:
spec.equal("username", username); // chỉ thêm khi username có giá trị
spec.like("email", email); // chỉ thêm khi email có giá trị
spec.in("status", statuses); // chỉ thêm khi statuses khác null và không rỗng
Tương đương cách viết tường minh:
spec.equal(username != null && !username.isEmpty(), "username", username);
spec.like(email != null && !email.isEmpty(), "email", email);
spec.in(statuses != null && !statuses.isEmpty(), "status", statuses);
Với logic phức tạp, dùng phiên bản có tham số match để kiểm soát chính xác điều kiện.
Các toán tử
Mỗi toán tử đều có phiên bản or* tương ứng, dùng để ghép điều kiện bằng OR:
| Nhóm | Method |
|---|---|
| Bằng/khác | equal, notEqual, orEqual, orNotEqual (có ignoreCase cho chuỗi) |
| Chuỗi | like, notLike, orLike, orNotLike, startsWith, endsWith, orStartsWith, orEndsWith |
| So sánh | greatThan, greatThanOrEqualTo, lessThan, lessThanOrEqualTo và biến thể or* |
| Khoảng | between, notBetween, orBetween, orNotBetween (nhận BetweenValue) |
| Tập hợp | in, notIn, orIn, orNotIn (nhận Collection hoặc varargs) |
| NULL | isNull, isNotNull, orIsNull, orIsNotNull |
Ví dụ với TypedJPASpecification:
TypedJPASpecification<Order> spec = Specifications.whereTyped()
.like(Order::getOrderNumber, request.getOrderNumber())
.between(Order::getOrderDate, request.getStartDate(), request.getEndDate())
.in(Order::getStatus, request.getStatuses())
.greatThanOrEqualTo(Order::getTotalAmount, request.getMinAmount())
.lessThanOrEqualTo(Order::getTotalAmount, request.getMaxAmount());
List<Order> orders = orderRepository.findAll(spec);
Ví dụ với tên cột dạng chuỗi:
JPASpecification<Order> spec = Specifications.where()
.equal("status", "ACTIVE")
.like("customer.name", keyword);
Kết hợp AND/OR theo nhóm
// (status = 'ACTIVE' OR status = 'PENDING') AND (age >= 18 AND age <= 60)
Specifications.where()
.equal("status", "ACTIVE")
.orEqual("status", "PENDING")
.and(subSpec -> subSpec
.greatThanOrEqualTo("age", 18)
.lessThanOrEqualTo("age", 60));
Sắp xếp ngay trên Specification
orderByAsc/orderByDesc nhận method reference (cả hai dạng specification) hoặc SingularAttribute của static metamodel:
Specifications.whereTyped()
.equal(Order::getStatus, "ACTIVE")
.orderByDesc(Order::getOrderDate);
Dựng điều kiện từ DTO annotation
Đánh dấu annotation điều kiện trên thuộc tính DTO rồi dựng Specification bằng ofBean/ofBeanTyped. Các annotation nằm trong package vn.govex.cloud.api:
| Annotation | Điều kiện sinh ra |
|---|---|
@Equals | field = value |
@NotEquals | field <> value |
@Like | field LIKE '%value%' |
@NotLike | field NOT LIKE '%value%' |
@StartsWith | field LIKE 'value%' |
@NotStartsWith | field NOT LIKE 'value%' |
@EndsWith | field LIKE '%value' |
@NotEndsWith | field NOT LIKE '%value' |
@LikePattern | field LIKE 'pattern' (pattern do người gọi truyền, không escape) |
@NotLikePattern | field NOT LIKE 'pattern' |
@GreaterThan | field > value |
@GreaterThanEqual | field >= value |
@LessThan | field < value |
@LessThanEqual | field <= value |
@Between | field BETWEEN start AND end |
@NotBetween | field NOT BETWEEN start AND end |
@In | field IN (values) |
@NotIn | field NOT IN (values) |
@LikeIn | field LIKE '%value1%' OR field LIKE '%value2%' cho từng giá trị trong collection |
@LikeOrLike | LIKE trên nhiều field cùng lúc (danh sách field phân tách dấu phẩy trong value) |
@IsNull | field IS NULL |
@IsNotNull | field IS NOT NULL |
@Length | So sánh độ dài chuỗi, chọn phép so sánh qua thuộc tính comparison (ComparisonType) |
Thuộc tính chung:
| Thuộc tính | Ý nghĩa |
|---|---|
value | Tên field tương ứng trong entity; để trống thì dùng tên thuộc tính DTO |
not | Khai báo điều kiện phủ định (tương ứng annotation @Not... là biến thể tường minh) |
operator | Cách ghép điều kiện: Operator.And (mặc định), Operator.Or, Operator.AndNot, Operator.OrNot |
fieldType | Chỉ có trên @In — gợi ý kiểu dữ liệu của field |
comparison | Chỉ có trên @Length — chọn phép so sánh độ dài |
Field có giá trị null/rỗng sẽ tự động bị bỏ qua khi dựng Specification.
public class OrderSearchDTO {
@Like("orderNumber")
private String orderNumber;
@Between("orderDate")
private DateRange orderDate; // extends BetweenValue<LocalDate>
@In("status")
private List<String> statuses;
@Equals("customer.id")
private Long customerId;
@GreaterThanEqual("totalAmount")
private BigDecimal minAmount;
@LessThanEqual("totalAmount")
private BigDecimal maxAmount;
// getters và setters
}
TypedJPASpecification<Order> spec = Specifications.ofBeanTyped(searchDTO);
List<Order> orders = orderRepository.findAll(spec);
Kết hợp DTO annotation với fluent API cho các điều kiện bổ sung:
TypedJPASpecification<User> spec = Specifications.ofBeanTyped(searchDTO)
.like(User::getAddress, addressKeyword)
.orEqual(User::getReferralCode, referralCode)
.and(subSpec -> subSpec
.greatThanOrEqualTo(User::getRegistrationDate, startDate)
.lessThanOrEqualTo(User::getRegistrationDate, endDate));
Bỏ qua hoặc đổi metadata một field bằng Paths
ofBean(bean, paths) cho phép override trường trước khi sinh điều kiện — ví dụ bỏ qua một field đang có annotation:
Paths paths = Paths.field("username")
.ignore(true)
.build();
JPASpecification<User> spec = Specifications.ofBean(searchDTO, paths);
Chỉ định alias/entityAlias cho trường (hữu ích khi ghép với JPQL JOIN nhiều entity):
Paths paths = Paths.field("email")
.entityAlias("u")
.field("department.name")
.entityAlias("d")
.build();
JPASpecification<User> spec = Specifications.ofBean(searchDTO, paths);
@JoinPath cho DTO
Khi field tìm kiếm nằm ở entity không xuất hiện trong JPQL của @Query, khai báo join ngay trên field DTO:
public class DonSearchParam {
@Like("soBanAn")
@JoinPath(path = "vuAn.banAn", type = BanAn.class)
private String soBanAnQD;
}
@JoinPath hỗ trợ path (đường dẫn join, có thể nested), joinType (mặc định LEFT), type (entity đích), alias và on (điều kiện ON khi entity không có quan hệ JPA).
Field & biểu thức truy vấn
Điều kiện typed dùng bộ mô tả trường chung của govex-cloud-data-common; dependency này được govex-cloud-data-jpa kéo sẵn qua transitive dependency nên không cần khai báo riêng:
Pathmô tả một trường: tên trường (name), alias hiển thị (alias), alias entity/join chứa trường (entityAlias), kiểu dữ liệu (type), kiểu entity (entityType), cờignorevà tập thuộc tính mở rộng. Chuỗi khởi tạo chỉ nhận dạngfieldhoặcalias.field; chuỗi có từ hai dấu chấm trở lên bị từ chối.Pathslà tậpPathđánh chỉ mục theo tên trường, khai báo fluent bằngPaths.field(...)và phải gọibuild()để chốt tập trường. TruyềnPathsvàoSpecifications.ofBean(bean, paths)/ofBeanTyped(bean, paths)khi cần cấu hình thêm alias/entityAlias/ignore cho trường trong DTO tìm kiếm.SFunc/TypedFieldlà tham chiếu getter an toàn kiểu (method reference) thay cho tên trường dạng chuỗi.Specifications.whereTyped()nhậnSFunc;Specifications.where()chỉ nhận tên trường dạng chuỗi.@Limitđặt trên query method để giới hạn số kết quả; module đọc giá trị này khi thực thi câu truy vấn, yêu cầuvalue > 0.
Path byField = Path.of("status");
Path byAlias = Path.of("o.status"); // alias entity/join đứng trước tên trường
Path byGetter = Path.of(Order::getStatus); // từ method reference, kèm kiểu entity
Paths paths = Paths.field("orderNumber")
.alias("soDon") // alias hiển thị của trường
.name(Order::getStatus) // khai báo trường kế tiếp từ getter
.build(); // chốt tập trường
TypedJPASpecification<Order> spec = Specifications.ofBeanTyped(searchDTO, paths)
.like(Order::getOrderNumber, searchDTO.getOrderNumber())
.in(Order::getStatus, searchDTO.getStatuses());
Với tham chiếu trường, luôn dùng API typed — Specifications.whereTyped() hoặc ofBeanTyped():
TypedJPASpecification<Order> spec = Specifications.whereTyped()
.like(Order::getOrderNumber, request.getOrderNumber())
.greatThanOrEqualTo(Order::getTotalAmount, request.getMinAmount());
@Limit dùng cho query method có @Query:
public interface OrderRepository extends BaseRepository<Order, String> {
@Limit(1)
@Query("select o from Order o where o.status = 'ACTIVE' order by o.orderDate desc")
List<Order> findFirstActive();
}
JOIN động, chọn cột và EXISTS
AbstractJPASpecification hỗ trợ JOIN động mà không cần quan hệ JPA trong entity.
JOIN theo entity class (cross join + ON condition khi không có relationship):
Specifications.where()
.leftJoin(Department.class).as("d")
.on(s -> s.equal("id", someValue));
JOIN giữa hai bảng không phải root (A → B → C):
Specifications.where()
.from(EntityB.class).as("b")
.leftJoin(EntityC.class).as("c")
.on(s -> s.equal("status", 1));
Điều kiện ON có 3 dạng: on(Specification), on(Function<JPASpecification<S>, JPASpecification<S>>) và onTyped(Function<TypedJPASpecification<S>, TypedJPASpecification<S>>).
Chọn cột động (thay cho SELECT *) và literal:
spec.select("id").select("name", "tenDonVi"); // theo tên path
spec.select(User_.id, User_.name); // theo static metamodel
spec.select(User::getId, User::getName); // theo method reference
spec.select(Department::getName, "tenPhong"); // kèm alias
spec.selectLiteral("ACTIVE", "trangThai"); // literal
Subquery EXISTS/NOT EXISTS theo hướng type-safe:
Specifications.whereTyped(Menu.class)
.existsTyped(Permission.class, s -> s
.equal(Permission::getUserId, currentUserId)
.equal(Permission::getDeleted, false));
Tương tự có notExists, orExists, orNotExists và biến thể *Typed.
So sánh cột-với-cột (không phải cột-với-giá trị), dùng SingularAttribute của static metamodel:
spec.equal(Order_.customerId, Customer_.id)
.notEqual(Order_.startDate, Order_.endDate)
.greatThan(Order_.actualAmount, Order_.estimatedAmount);
JPQL/native query trả về VO
| Annotation | Loại query | resultTransformer mặc định |
|---|---|---|
@JpaQuery | JPQL — meta-annotation của @Query | PropertyTupleTransformer khớp alias theo tên property, không phân biệt hoa thường |
@JpaNativeQuery | Native SQL — meta-annotation của @NativeQuery | UnderscoreTransformer chuyển alias snake_case sang camelCase |
Có thể chỉ định resultTransformer khác (class kế thừa AbstractTupleTransformer). VO chỉ cần field trùng tên alias, có constructor không tham số; framework tự tạo instance và set property qua BeanWrapper — không cần interface projection hay constructor expression:
@JpaQuery("SELECT u.id as id, u.name as ten, d.name as tenDonVi " +
"FROM User u LEFT JOIN DonVi d ON u.maDonVi = d.ma")
List<UserVO> search(Specification<User> spec);
UserVO chỉ cần các field id, ten, tenDonVi. Chi tiết native query và SQLExpression xem Native query & SQLExpression.
Kết hợp @Query với Specification
Repository khai báo method với @Query (phần cố định) và tham số Specification (phần động):
public interface UserRepository extends BaseRepository<User, String> {
@Query("select u from User u where u.active = true")
List<User> findActiveUsers(Specification<User> spec);
@Query("select u from User u where u.registrationDate >= :startDate")
List<User> findByRegistrationDate(@Param("startDate") LocalDate startDate, Specification<User> spec);
}
Quy tắc kết hợp:
- Specification
nullhoặc rỗng: câu JPQL chạy nguyên trạng. - Điều kiện từ Specification luôn được nối vào sau điều kiện cố định bằng AND.
- Tham số named parameter của
@Queryvẫn hoạt động bình thường.
Cơ chế nội bộ: SpecificationUtils.applySpecification() copy SQM tree của Hibernate, resolve root tương ứng, merge predicate vào WHERE hiện có (kể cả derived root/subquery trong FROM), merge selection nếu Specification có chọn cột động và xử lý tương tự cho câu DELETE/UPDATE.
Phân trang & sắp xếp
- Truyền
PageablevàofindAll(spec, pageable)để nhậnPage; điều kiện Specification được áp dụng cho cả câu query và câu count. - Sắp xếp có thể khai báo trực tiếp trên Specification (
orderByAsc/orderByDesc) hoặc trongPageable/Sort. - Giới hạn số bản ghi bằng
Limitcủa Spring Data quafindAll(spec, limit)/findAll(spec, sort, limit).
Pageable pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "orderDate"));
TypedJPASpecification<Order> spec = Specifications.ofBeanTyped(request);
Page<OrderVO> page = orderRepository.findAll(spec, pageable)
.map(mapper::toVO);
Kết quả đơn và projection
@QuerySingleResult biến kết quả rỗng thành null (kiểu tham chiếu) hoặc Optional.empty() thay vì ném EmptyResultDataAccessException/NoResultException:
@QuerySingleResult
@Query("select u from User u where u.username = :username")
User findByUsernameNullable(@Param("username") String username);
MapToProjectionConverter tự chuyển Map<String, Object> sang class implements vn.govex.cloud.api.Projection theo tên field.
EntitySchemaUpdater
Tự động migrate schema cho một số entity cụ thể khi application startup (Hibernate hbm2ddl update), chỉ dùng cho module cần tự quản lý schema riêng:
@Bean
EntitySchemaUpdater messageSchemaUpdater(DataSource dataSource) {
return new EntitySchemaUpdater(dataSource, Message.class, MessageTracking.class);
}
Luồng hoạt động
Sơ đồ dưới đây mô tả luồng truy vấn động: từ DTO annotation hoặc @Query tới Specification, câu JPQL/SQL thực thi và ánh xạ kết quả về VO.
Lưu ý
@EnableExtendJpaRepositorieslà điều kiện để các tính năng mở rộng hoạt động đầy đủ (base repository, statement inspector, projection converter,@QuerySingleResult).where()nhận tên cột dạng chuỗi;whereTyped()/ofBeanTyped()mới nhận method reference.- Instance Specification giữ trạng thái trong lúc dựng truy vấn — không chia sẻ giữa nhiều luồng.
saveBatchchỉ dùng cho insert,updateBatchchỉ dùng cho update;saveOrUpdateAllByNotNullPropertieskiểm tra tồn tại từng bản ghi nên chậm hơn batch thuần túy.- JPA auditing lấy người thao tác từ
SecurityContext; khi không có thông tin xác thực,createdBy/updatedByđể trống và module chỉ ghi cảnh báo một lần. - Dependency chứa lớp override trong package gốc của Spring Data JPA/Hibernate; khi nâng cấp hai thư viện này cần kiểm tra lại tương thích.
- Static metamodel (
Order_) chỉ có khi cấu hìnhhibernate-jpamodelgentrong build.