This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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
    )