基于派生属性在Spring Data REST中实现筛选与排序
Great question! I’ve run into this exact scenario countless times—Spring Data REST handles persistent entity fields seamlessly, but derived properties (like embedded object fields or computed values) need a bit of extra work. Let’s break down the most practical approaches based on your setup (using JpaRepository, QueryDslPredicateExecutor, and QuerydslBinderCustomizer):
1. Customize Querydsl Bindings for Derived Properties
Since you’re already implementing QuerydslBinderCustomizer, this is the most straightforward way to map request parameters to derived properties, whether they’re embedded fields or computed values.
Example 1: Handling Embedded Properties
Say your Person entity has an embedded Address with a city field:
@Getter @Entity public class Person extends AbstractEntity<Long> { @Embedded private Address address; // other persistent fields... } @Embeddable public class Address { private String city; // other address fields... }
You can bind the city request parameter directly to the embedded path in your repository:
public interface PersonRepository extends JpaRepository<Person, Long>, QueryDslPredicateExecutor<Person>, QuerydslBinderCustomizer<QPerson> { @Override default void customize(QuerydslBindings bindings, QPerson root) { // Map ?city=Paris to p.address.city = Paris bindings.bind(root.address.city).first((path, value) -> path.eq(value)); // Optional: Ignore default bindings for unlisted fields to keep endpoints clean // bindings.excludeUnlistedProperties(true); // bindings.including(root.address.city, root.firstName); } }
Example 2: Handling Computed Derived Properties
For computed properties like fullName (a combination of firstName and lastName), you can manually define the query logic when the request parameter matches:
@Override default void customize(QuerydslBindings bindings, QPerson root) { // Handle ?fullName=John by checking firstName OR lastName for a match bindings.bind(String.class).first((SingleValueBinding<StringPath, String>) (path, value) -> { if ("fullName".equals(path.getMetadata().getName())) { return root.firstName.containsIgnoreCase(value) .or(root.lastName.containsIgnoreCase(value)); } // Fallback to default behavior for other string fields return path.containsIgnoreCase(value); }); }
2. Define Custom Query Methods with @Query
If you have fixed filtering logic for a derived property, creating a custom query method lets Spring Data REST expose it as a dedicated search endpoint.
Example: Filter by Embedded City or Computed FullName
public interface PersonRepository extends JpaRepository<Person, Long>, QueryDslPredicateExecutor<Person>, QuerydslBinderCustomizer<QPerson> { // Exposes /people/search/findByAddressCity?city=London @Query("SELECT p FROM Person p WHERE p.address.city = :city") Page<Person> findByAddressCity(@Param("city") String city, Pageable pageable); // Exposes /people/search/findByFullNameContaining?fullName=Doe @Query("SELECT p FROM Person p WHERE CONCAT(p.firstName, ' ', p.lastName) LIKE %:fullName%") Page<Person> findByFullNameContaining(@Param("fullName") String fullName, Pageable pageable); }
This approach is perfect for complex logic that’s hard to express with Querydsl bindings, and it automatically supports pagination/sorting via the Pageable parameter.
3. Sorting by Derived Properties
Sorting is trickier because Spring Data REST defaults to only sorting on persistent database fields. Here’s how to handle it:
Option 1: Querydsl-Based Sorting
Extend your QuerydslBinderCustomizer configuration to allow sorting on derived paths:
@Override default void customize(QuerydslBindings bindings, QPerson root) { // Allow sorting by address.city via ?sort=address.city,asc bindings.including(root.address.city); // For computed fullName, map ?sort=fullName,asc to sorting by firstName + lastName bindings.bind(String.class).all((path, values) -> { if ("fullName".equals(path.getMetadata().getName())) { SortDirection direction = values.size() == 2 && "desc".equalsIgnoreCase(values.get(1)) ? SortDirection.DESC : SortDirection.ASC; return Optional.of(direction.isAscending() ? root.firstName.asc().and(root.lastName.asc()) : root.firstName.desc().and(root.lastName.desc())); } return Optional.empty(); }); }
Option 2: JPQL Query with Explicit Sorting
For computed properties that don’t map directly to a database path, define a query that includes the sort expression:
@Query("SELECT p FROM Person p ORDER BY CONCAT(p.firstName, ' ', p.lastName) ASC") Page<Person> findAllSortedByFullName(Pageable pageable);
Or, if you want dynamic sorting, use a custom Sort parameter and resolve it in the query:
@Query("SELECT p FROM Person p ORDER BY " + "CASE WHEN :sortBy = 'fullName' THEN CONCAT(p.firstName, ' ', p.lastName) END ASC") Page<Person> findAllWithDynamicSort(@Param("sortBy") String sortBy, Pageable pageable);
Key Notes
- For embedded properties, always prefer Querydsl bindings—they keep your code clean and align with Spring Data REST’s conventions.
- For computed properties, test performance: using
CONCATor similar functions in JPQL/Querydsl can impact query speed on large datasets (consider adding a generated database column if needed). - Always test your endpoints with tools like Postman to ensure parameters map correctly to your derived properties.
内容的提问来源于stack exchange,提问作者Patrik Mihalčin

