Simple Specs

在 POJO 中, 你可以在 Field 上使用 @Spec 來定義 Specification 的實作, 預設是 Equals:

@Spec // 同等於 @Spec(Equals.class)
String firstname;

對應的 entity path 預設會使用 field name, 你也可以設定 @Spec#path 來改變

@Spec(path = "...") // 有定義則優先使用
String firstname; // 預設使用欄位名稱

Built-in @Spec

以下是內建的 @Spec 的類型清單:

SpecSupported field typeSampleJPQL snippet
EqualsAny@Spec(Equals.class) String firstname;... where x.firstname = ?
NotEqualsAny@Spec(NotEquals.class) String firstname;... where x.firstname <> ?
BetweenIterable of Comparable
(Expected exact 2 elements in Iterable)
@Spec(Between.class) List<Integer> age;... where x.age between ? and ?
LessThanComparable@Spec(LessThan.class) Integer age;... where x.age < ?
LessThanEqualComparable@Spec(LessThanEqual.class) Integer age;... where x.age <= ?
GreaterThanComparable@Spec(GreaterThan.class) Integer age;... where x.age > ?
GreaterThanEqualComparable@Spec(GreaterThanEqual.class) Integer age;... where x.age >= ?
AfterComparable@Spec(After.class) LocalDate startDate;... where x.startDate > ?
BeforeComparable@Spec(Before.class) LocalDate startDate;... where x.startDate < ?
IsNullBoolean@Spec(IsNull.class) Boolean age;... where x.age is null (if true)
... where x.age not null (if false)
NotNullBoolean@Spec(NotNull .class) Boolean age;... where x.age not null (if true)
... where x.age is null (if false)
LikeString@Spec(Like.class) String firstname;... where x.firstname like %?%
NotLikeString@Spec(NotLike.class) String firstname;... where x.firstname not like %?%
StartingWithString@Spec(StartingWith.class) String firstname;... where x.firstname like ?%
EndingWithString@Spec(EndingWith.class) String firstname;... where x.firstname like %?
InIterable of Any@Spec(In.class) Set<String> firstname;... where x.firstname in (?, ?, ...)
NotInIterable of Any@Spec(NotIn.class) Set<String> firstname;... where x.firstname not in (?, ?, ...)
TrueBoolean@Spec(True.class) Boolean active;... where x.active = true (if true)
... where x.active = false (if false)
FalseBoolean@Spec(False.class) Boolean active;... where x.active = false (if true)
... where x.active = true (if false)
HasLengthBoolean@Spec(HasLength.class) Boolean firstname;... where x.firstname is not null and character_length(x.firstname)>0 (if true)
... where not(x.firstname is not null and character_length(x.firstname)>0) (if false)
HasTextBoolean@Spec(HasText.class) Boolean firstname;... where x.firstname is not null and character_length(trim(BOTH from x.firstname))>0 (if true)
... where not(where x.firstname is not null and character_length(trim(BOTH from x.firstname))>0) (if false)

為了方便已經熟悉 Spring Data JPA 的人使用, 以上名稱都是儘量跟著 Query Methods 一樣

Negates @Spec

你可以使用 @Spec#not 來判斷反向條件, 預設是 false, 設定成 true 就會將結果做反向轉換.

例如, 我想要用 Between不在區間內的資料, 則範例如下:

@Spec(value = Between.class, not = true)
Collection<Integer> age;

執行的 SQL 會類似:

... where x.age not between ? and ?

Extending @Spec

@Spec 是可以很容易擴充的, 只要實作了 SimpleSpecification<T> 並提供規定的 Constructor, 這些 class 就可以被定義在 @Spec

例如, 我有個 Customer Entity, 有以下欄位:

  • firstname (String) - 人名, 可重複
  • createdTime (LocalDateTime) - 建立時間, 不可重複

我希望可以找出每個人名中, 建立時間為最新的那筆資料! 且我打算撰寫一個 subquery 來完成這需求, 完整的範例如下:

首先我們實作 SimpleSpecification<T>, 並提供規定的 Constructor:

public class MaxCustomerCreatedTime extends SimpleSpecification<Customer> {

  // 這是規定必須提供的建構值, 修飾詞不限定: public, protected, default, private 都支援
  protected MaxCustomerCreatedTime(Context context, String path, Object value) {
    super(context, path, value);
  }

  @Override
  public Predicate toPredicate(Root<Customer> root,
    CriteriaQuery<?> query,
    CriteriaBuilder builder) {
    // 以下提供 subquery 的實作, 僅供參考
    var subquery = query.subquery(LocalDateTime.class);
    var subroot = subquery.from(Customer.class);
    subquery.select(
      builder.greatest(subroot.get("createdTime").as(LocalDateTime.class))
    ).where(builder.equal(root.get((String) value), subroot.get((String) value)));
    return builder.equal(root.get("createdTime"), subquery);
  }
}

上面完成的 MaxCustomerCreatedTime 就可以被應用在 @Spec 中了, 接著我們定義 POJO 及進行 Specification 的轉換:

@Data
public class CustomerCriteria {

  @Spec(MaxCustomerCreatedTime.class)
  String maxBy;
}

var criteria = new CustomerCriteria();
criteria.setMaxBy("firstname");

var spec = mapper.toSpec(criteria, Customer.class);
repository.findAll(spec);

執行的 SQL 會類似:

... where customer0_.created_time=(
  select max(customer1_.created_time) from customer customer1_ 
  where customer0_.firstname=customer1_.firstname
)