This is the multi-page printable view of this section. Click here to print.
Documentation
- 1: Mapper
- 1.1: SpecMapper
- 1.2: Simple Specs
- 1.3: Combine Specs
- 1.4: Nested Specs
- 1.5: Join
- 1.6: Join Fetch
- 1.7: Customize Specs
- 1.8: Logging
- 1.9: Limitation
- 2: Starter
- 2.1: Query by Spec
- 2.2: Config SpecMapper
- 3: Compatibility
1 - Mapper
<dependency>
<groupId>tw.com.softleader.data.jakarta</groupId>
<artifactId>specification-mapper</artifactId>
<version>${specification-mapper.version}</version>
</dependency>
requires specification.mapper;
requires jakarta.persistence;
logging:
level:
tw.com.softleader.data.jpa.spec: info
從 Maven Central 查看最新版本
specification-mapper 是一套 Specifications 的產生器, 它讀取了 Object 中的欄位, 配合欄位上 Annotation 的定義, 來動態的建立查詢條件!
另外 specification-mapper-starter 提供了 Spring Boot 的整合, 讓你可以零配置的在 Spring apps 中使用, 使用 Spring boot 的應用程式可以參考看看!
1.1 - SpecMapper
SpecMapper
是所有 Spec 操作的 API 入口, 首先我們需要建構實例:
var mapepr = SpecMapper.builder().build();
接著我們定義封裝查詢條件的物件, 這是一個 POJO 即可, 如:
@Data
public class CustomerCriteria {
@Spec(Like.class)
String firstname;
}
這樣我們就可以做 Specification
的轉換了, 得到 Specification
後就可以依照原本的方式去資料庫查詢, 例如透過 Spring Data JPA 的 repository:
var criteria = new CustomerCriteria();
criteria.setFirstname("Hello")
var mapper = SpecMapper.builder().build();
var specification = mapper.toSpec(criteria);
customerRepository.findAll(specification);
執行 SQL 將會是:
... where x.firstname like '%Hello%'
Skipping Strategy
在 POJO 中的欄位, 只要符合以下任一條件, 在轉換的過程中都將會忽略:
- 沒有掛任何 Spec Annotation
- 值為 null
- 若 Type 為 Iterable 且值為 empty
- 若 Type 為 Optional 且值為 empty
- 若 Type 為 CharSequence 且長度為 0
- 若 Type 為 Array 且長度為 0
- 若 Type 為 Map 且值為 empty
例如, 將以下 POJO 建構後, 不 set 任何值就直接轉換成 Specification
及查詢
@Data
public class CustomerCriteria {
@Spec(Like.class)
String firstname;
String lastname = "Hello";
@Spec
String nickname = "";
@Spec(GreaterThat.class)
Optional<Integer> age = Optional.empty();
@Spec(In.class)
Collection<String> addresses = Arrays.asList();
}
var mapper = SpecMapper.builder().build();
customerRepository.findAll(mapper.toSpec(new CustomerCriteria()));
以上執行的 SQL 將不會有任何過濾條件!
Note
如果你有使用 Builder Pattern, (e.g. Lombok’s @Builder), 請特別注意 Builder 的 Default Value!若你想要客製化跳脫邏輯, 可以實作 SkippingStrategy
, 並在建構 SpecMapper
時傳入:
var mapper = SpecMapper.builder()
.defaultResolvers()
.skippingStrategy(fieldValue -> {
// 判斷是否要跳 field value, 回傳 boolean
})
.build();
1.2 - 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
的類型清單:
Spec | Supported field type | Sample | JPQL snippet |
---|---|---|---|
Equals | Any | @Spec(Equals.class) String firstname; | ... where x.firstname = ? |
NotEquals | Any | @Spec(NotEquals.class) String firstname; | ... where x.firstname <> ? |
Between | Iterable of Comparable (Expected exact 2 elements in Iterable) | @Spec(Between.class) List<Integer> age; | ... where x.age between ? and ? |
LessThan | Comparable | @Spec(LessThan.class) Integer age; | ... where x.age < ? |
LessThanEqual | Comparable | @Spec(LessThanEqual.class) Integer age; | ... where x.age <= ? |
GreaterThan | Comparable | @Spec(GreaterThan.class) Integer age; | ... where x.age > ? |
GreaterThanEqual | Comparable | @Spec(GreaterThanEqual.class) Integer age; | ... where x.age >= ? |
After | Comparable | @Spec(After.class) LocalDate startDate; | ... where x.startDate > ? |
Before | Comparable | @Spec(Before.class) LocalDate startDate; | ... where x.startDate < ? |
IsNull | Boolean | @Spec(IsNull.class) Boolean age; | ... where x.age is null (if true)... where x.age not null (if false) |
NotNull | Boolean | @Spec(NotNull .class) Boolean age; | ... where x.age not null (if true)... where x.age is null (if false) |
Like | String | @Spec(Like.class) String firstname; | ... where x.firstname like %?% |
NotLike | String | @Spec(NotLike.class) String firstname; | ... where x.firstname not like %?% |
StartingWith | String | @Spec(StartingWith.class) String firstname; | ... where x.firstname like ?% |
EndingWith | String | @Spec(EndingWith.class) String firstname; | ... where x.firstname like %? |
In | Iterable of Any | @Spec(In.class) Set<String> firstname; | ... where x.firstname in (?, ?, ...) |
NotIn | Iterable of Any | @Spec(NotIn.class) Set<String> firstname; | ... where x.firstname not in (?, ?, ...) |
True | Boolean | @Spec(True.class) Boolean active; | ... where x.active = true (if true)... where x.active = false (if false) |
False | Boolean | @Spec(False.class) Boolean active; | ... where x.active = false (if true)... where x.active = true (if false) |
HasLength | Boolean | @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) |
HasText | Boolean | @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
)
1.3 - Combine Specs
在 POJO 中, 可以使用 @And
或 @Or
來組合一個物件中的多個 Specification, 組合的預設是 @And
.
Combine on Classes
@And
或 @Or
可以宣告在 Class 上, 例如:
@Or // 若沒定義預設就是 @And
@Data
public class CustomerCriteria {
@Spec(Like.class)
String firstname;
@Spec(Like.class)
String lastname;
}
執行的 SQL 會類似:
... where x.firstname like %?% or x.lastname like %?%
Combine on Fields
你也可以將 @And
或 @Or
註記在欄位上來控制單獨一個欄位要怎麼跟其他欄位組合. 舉個例子如下:
@Data
public class CustomerCriteria {
@Spec(Like.class)
String firstname;
@Spec(Like.class)
String lastname;
@Or
@Spec(value = After.class, not = true)
LocalDate birthday;
}
執行的 SQL 會類似:
... where (x.firstname like ?) and (x.lastname like ?) or x.birthday<=?
Important
欄位是依照宣告的順序組合的, 而 SQL 也是有運算子優先順序的, 需注意兩者的配合的結果是否符合你的期望例如, 將上面的例子調整欄位順序:
@Data
public class CustomerCriteria {
@Spec(Like.class)
String firstname;
@Or
@Spec(value = After.class, not = true)
LocalDate birthday;
@Spec(Like.class)
String lastname;
}
執行的 SQL 會類似:
... where (x.firstname like ? or x.birthday<=?) and (x.lastname like ?)
1.4 - Nested Specs
在 POJO 中, 你可以在 Field 上使用 @NestedSpec
來告知 SpecMapper
要往下一層物件 (Nested Object) 去組合 Specification, 這是沒有層級限制的, 可以一直往下找!
例如我有一個共用的 AddressCriteria
POJO, 我就可以將它掛載到其他的 POJO 中, 程式碼範例如下:
@Data
public class CustomerCriteria {
@Spec(Like.class)
String firstname;
@NestedSpec
AddressCriteria address;
}
@Or
@Data
public class AddressCriteria {
@Spec
String county;
@Spec
String city;
}
執行的 SQL 會類似:
... where x.firstname like %?% and ( x.county=? or x.city=? )
Combine Nested Specs
你也可以在 Nested Object 的欄位上宣告 @And
或 @Or
來控制結果要怎麼跟其他的欄位組合, 詳細的說明請參考 Combining on Fields.
舉個例子如下:
@Data
public class CustomerCriteria {
@Spec(Like.class)
String firstname;
@Or
@NestedSpec
AddressCriteria address;
}
@Data
public class AddressCriteria {
@Spec
String county;
@Spec
String city;
}
執行的 SQL 會類似:
... where (x.firstname like ?) or x.county=? and x.city=?
1.5 - Join
在 POJO 中, 你可以在 Field 或 Class 上使用 @Join
來過濾關聯的 Entity
Single Join
要使用 @Join
, 在 Entity 中需要先定義好關聯, 例如, 有個客戶 Entity, 會一對多的關聯訂單 Entity:
@Entity
class Customer {
@OneToMany(cascade = ALL, fetch = LAZY)
@JoinColumn(name = "order_id")
private Collection<Order> orders;
}
@Entity
class Order {
private String itemName;
}
如果你想要查詢買了指定東西的客戶, 則可以定義 POJO 如下:
@Data
public class CustomerOrderCriteria {
@Join(path = "orders", alias = "o")
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
}
執行的 SQL 會類似:
select distinct customer0_.* from customer customer0_
inner join orders orders1_ on customer0_.id=orders1_.order_id
where orders1_.item_name in (? , ?)
@Join
也可以用在 class 層級, 在同一個物件內的欄位就都可以使用 alias
來對 Join 的對象增加條件, 例如:
@Data
@Join(path = "orders", alias = "o")
public class CustomerOrderCriteria {
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
@Spec(path = "o.orderNo", value = StartingWith.class)
String orderNo;
}
Join Behavior
為了比較符合大部分的使用情境, 以下是預設的行為:
- Join type 預設為
INNER
- 將結果排除重複 (
distinct
)
透過設定 @Join#joinType
或 @Join#distinct
可以改變預設行為, 如:
@Join(joinType = JoinType.RIGHT, distinct = false)
Multi Joins
你可以使用 @Joins
來定義多層級的 Join, 例如, 在剛剛的訂單 Entity 中, 還會多對多的關聯到類別 Entity:
@Entity
class Customer {
@OneToMany(cascade = ALL, fetch = LAZY)
@JoinColumn(name = "order_id")
private Set<Order> orders;
}
@Entity
class Order {
@ManyToMany(cascade = ALL, fetch = LAZY)
private Set<Tag> tags;
}
@Entity
class Tag {
private String name;
}
如果你想要查詢買了指定所屬類別的東西的客戶, 則可以定義 POJO 如下:
@Data
class CustomerOrderTagCriteria {
@Joins({
@Join(path = "orders", alias = "o"),
@Join(path = "o.tags", alias = "t")
})
@Spec(path = "t.name", value = In.class)
Collection<String> tags;
}
執行的 SQL 會類似:
select distinct customer0_.* from customer customer0_
inner join orders orders1_ on customer0_.id=orders1_.order_id
inner join orders_tags tags2_ on orders1_.id=tags2_.order_id
inner join tag tag3_ on tags2_.tags_id=tag3_.id
where tag3_.name in (?)
@Joins
也是可以用在 class 層級, 在同一個物件內的欄位就都可以使用 alias
來對 Join 的對象增加條件, 例如:
@Data
@Joins({
@Join(path = "orders", alias = "o"),
@Join(path = "o.tags", alias = "t")
})
public class CustomerOrderCriteria {
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
@Spec(path = "t.name", value = In.class)
Collection<String> tags;
}
Joins Order
Annotation 的處理是有順序性的, 因此必須依照 Join 的順序去定義 @Joins
例如依照上面的情境, 下列的定義順序是錯誤的:
@Data
class CustomerOrderTagCriteria {
@Joins({
@Join(path = "o.tags", alias = "t"), // "o" alias will be not exist during processing this @Join
@Join(path = "orders", alias = "o")
})
@Spec(path = "t.name", value = In.class)
Collection<String> tagNames;
}
Alias
@Join#alias
的使用規則如下:
- 在同個 POJO 中是共用的
- 在同的 POJO 中不可重複宣告
- 若沒提供, 預設使用
@Join#path
- 若包含了
.
會以_
取代之
例如:
@Joins({
@Join(path = "orders"), // alias 預設為 orders
@Join(path = "orders.tags") // alias 預設為 orders_tags
})
@Spec(path = "orders_tags.name", value = In.class)
1.6 - Join Fetch
在 POJO 中, 你可以在 Field 或 Class 上使用 @JoinFetch
來過濾關聯的 Entity, 跟 @Join
的差別是, 這可以一次撈出所有 Lazy 的關聯資料
Single Join Fetch
例如, 有個客戶 Entity, 會一對多的關聯訂單 Entity:
@Entity
class Customer {
@OneToMany(fetch = LAZY, cascade = ALL)
@JoinColumn(name = "order_id")
private Collection<Order> orders;
}
@Entity
class Order {
private String itemName;
}
如果你想在取得 Customer 時就順便取得 Order, 則:
@Data
@JoinFetch(path = "orders")
class CustomerOrderCriteria {
@Spec
String name;
}
執行的 SQL 會類似:
select distinct
customer0_.* ...,
orders1_.* ...
from customer customer0_
inner outer join orders orders1_ on customer0_.id=orders1_.order_id
where customer0_.name=?
你可以看到
orders1_.*
也被放入了 select 項目內
Join Behavior
為了比較符合大部分的使用情境, 以下是預設的行為:
- Join type 預設為
INNER
- 將結果排除重複 (
distinct
)
透過設定 @JoinFetch#joinType
或 @JoinFetch#distinct
可以改變預設行為, 如:
@JoinFetch(joinType = JoinType.RIGHT, distinct = false)
With Clause
@JoinFetch
也可以讓你過濾資料, 例如, 我想要過濾客戶名稱及訂單名稱, 則可以定義 POJO 如下:
@Data
@JoinFetch(path = "orders", alias = "o")
public class CustomerOrderCriteria {
@Spec
String name;
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
}
執行的 SQL 會類似:
select distinct
customer0_.* ...,
orders1_.* ...
from customer customer0_
inner outer join orders orders1_ on customer0_.id=orders1_.order_id
where customer0_.name=? and orders1_.item_name in (?)
@JoinFetch
也可以用在 Field 層級, 例如:
@Data
public class CustomerOrderCriteria {
@Spec
String name;
@JoinFetch(path = "orders", alias = "o")
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
}
Multi Join Fetches
你可以使用 @JoinFetches
來定義多層級的 Fetch, 例如, 在剛剛的訂單 Entity 中, 還會多對多的關聯到類別 Entity:
@Entity
class Customer {
@OneToMany(cascade = ALL, fetch = LAZY)
@JoinColumn(name = "order_id")
private Set<Order> orders;
}
@Entity
class Order {
@ManyToMany(cascade = ALL, fetch = LAZY)
private Set<Tag> tags;
}
@Entity
class Tag {
private String name;
}
如果你想要查詢買了指定所屬類別的東西的客戶, 則可以定義 POJO 如下:
@Data
class CustomerOrderTagCriteria {
@JoinFetches({
@JoinFetch(path = "orders", alias = "o"),
@JoinFetch(path = "o.tags", alias = "t")
})
@Spec(path = "t.name", value = In.class)
Collection<String> tags;
}
執行的 SQL 會類似:
select distinct
customer0_.* ...,
orders1_.* ...,
tag3_.* ...
from customer customer0_
inner join orders orders1_ on customer0_.id=orders1_.order_id
inner join orders_tags tags2_ on orders1_.id=tags2_.order_id
inner join tag tag3_ on tags2_.tags_id=tag3_.id
where tag3_.name in (?)
@JoinFetches
也是可以用在 class 層級, 在同一個物件內的欄位就都可以使用 alias
來對 Join 的對象增加條件, 例如:
@Data
@JoinFetches({
@JoinFetch(path = "orders", alias = "o"),
@JoinFetch(path = "o.tags", alias = "t")
})
public class CustomerOrderCriteria {
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
@Spec(path = "t.name", value = In.class)
Collection<String> tags;
}
Fetches Order
Annotation 的處理是有順序性的, 因此必須依照 Join 的順序去定義 @JoinFetches
例如依照上面的情境, 下列的定義順序是錯誤的:
@Data
class CustomerOrderTagCriteria {
@JoinFetches({
@JoinFetch(path = "o.tags", alias = "t"), // "o" alias will be not exist during processing this @Join
@JoinFetch(path = "orders", alias = "o")
})
@Spec(path = "t.name", value = In.class)
Collection<String> tagNames;
}
Alias
@JoinFetch#alias
的使用規則如下:
- 在同個 POJO 中是共用的
- 在同的 POJO 中不可重複宣告
- 若沒提供, 預設使用
@JoinFetch#path
- 若包含了
.
會以_
取代之
例如:
@JoinFetches({
@JoinFetch(path = "orders"), // alias 預設為 orders
@JoinFetch(path = "orders.tags") // alias 預設為 orders_tags
})
@Spec(path = "orders_tags.name", value = In.class)
1.7 - Customize Specs
延續 Extending @Spec 章節範例, 進階一點現在我們希望可以將 Entity Class 設計成可以配置, 這樣才能在 Customer 以外的 Entity 都可以使用!
要完成這需求我們需要在 Annotation 中定義更多參數, 因此 Simple @Spec 不適用了, 我們需要的是定義新的 Annotation, 完整的程式碼如下:
首先我們定義 Annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface MaxCreatedTime {
Class<?> from();
}
接著我們要撰寫負責處理 @MaxCreatedTime
的邏輯, 透過實作 SpecificationResolver
來擴充:
public class MaxCreatedTimeSpecificationResolver implements SpecificationResolver {
@Override
public boolean supports(Databind databind) {
// 這邊告訴 SpecMapper 什麼時候要使用此 resolver
return databind.getField().isAnnotationPresent(MaxCreatedTime.class);
}
@Override
public Specification<Object> buildSpecification(Context context, Databind databind) {
var def = databind.getField().getAnnotation(MaxCreatedTime.class);
return databind.getFieldValue()
.map(value -> subquery(def.from(), value.toString()))
.orElse(null);
}
Specification<Object> subquery(Class<?> entityClass, String by) {
// 以下提供 subquery 的實作, 僅供參考
return (root, query, builder) -> {
var subquery = query.subquery(LocalDateTime.class);
var subroot = subquery.from(entityClass);
subquery.select(
builder.greatest(subroot.get("createdTime").as(LocalDateTime.class))
).where(builder.equal(root.get(by), subroot.get(by)));
return builder.equal(root.get("createdTime"), subquery);
};
}
}
接著我們在 SpecMapper
建構時加入此 resolver:
var mapper = SpecMapper.builder()
.defaultResolvers()
.resolver(new MaxCreatedTimeSpecificationResolver())
.build();
最後我們定義 POJO 及進行 Specification
的轉換:
@Data
public class CustomerCriteria {
@MaxCreatedTime(from = Customer.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
)
1.8 - Logging
在 Logging 等級中設定 tw.com.softleader.data.jpa.spec.SpecMapper=debug
, 會在物件轉換成 Spec 的過程中印出更多資訊, 可以有效的幫助查找問題, 輸出類似如:
DEBUG 20297 --- [ main] t.c.softleader.data.jpa.spec.SpecMapper : --- Spec AST ---
+-[CustomerCriteria]: my.package.CustomerCriteria
| +-[CustomerCriteria.firstname]: @Spec(value=Equals, path=, not=false) -> Equals[path=name, value=matt]
| +-[CustomerCriteria.address]: my.package.AddressCriteria (NestedSpecificationResolver)
| | +-[AddressCriteria.county]: @Spec(value=Equals, path=, not=false) -> null
| | +-[AddressCriteria.city]: @Spec(value=Equals, path=, not=false) -> Equals[path=name, value=Taipei]
| \-[CustomerCriteria.address]: Conjunction[specs=[Equals[path=city, value=Taipei]]]
\-[CustomerCriteria]: Conjunction[specs=[Equals[path=name, value=matt], Conjunction[specs=[Equals[path=city, value=Taipei]]]]]
Logger Name
從上面範例的第一行可以看到, 預設的 Logger Name 是使用 SpecMapper
, 這可以方便你統一的設定, 如果你喜歡根據轉換的物件來控制和設定 Logging, 我們提供了另一種策略, 將 Logger Name 改成使用目標物件的 Logger 來輸出
通過設定 ASTWriterFactory
來調整策略, 你可以調整例如:
var mapper = SpecMapper.builder()
.defaultResolvers()
// 預設為 ASTWriterFactory.domain()
.astWriterFactory(ASTWriterFactory.impersonation())
.build();
輸出會類似:
DEBUG 20297 --- [ main] my.package.CustomerCriteria : --- Spec AST ---
...
1.9 - Limitation
SpecMapper
在找 POJO 的欄位時, 只會找當前 Class 的 Local Field, 而不去往上找 Hierarchy Classes 的 Field, 如果你共用的欄位想要用在多個 POJO, 請考慮使用 Nested Specs 方式
2 - Starter
<dependency>
<groupId>tw.com.softleader.data.jakarta</groupId>
<artifactId>specification-mapper-starter</artifactId>
<version>${specification-mapper.version}</version>
</dependency>
requires specification.mapper;
requires specification.mapper.starter;
requires jakarta.persistence;
logging:
level:
tw.com.softleader.data.jpa.spec.starter: info
從 Maven Central 查看最新版本
specification-mapper-starter 整合了 specification-mapper 及 Spring Data JPA, 並提供了 Query by Spec 的查詢方式等
Query by Spec (QBS) 是一個 user-friendly 的查詢方式, 可以動態的建立查詢條件 (Specifications), 透過 QBS interface 就可以執行查詢語句!
Getting Started
只要在 pom.xml
中加入 dependency, 此 Starter 在 Spring Boot 啟動過程就會自動的配置一切, 讓你可以零配置的就開始使用, 包含了:
- Query By Spec 的設定
- 註冊預設的
SpecMapper
自動配置預設是啟用的, 你可以透過 properties 中的 spec.mapper.enabled
控制, 如要關閉則:
spec:
mapper:
enabled: false
2.1 - Query by Spec
Query by Spec (QBS) 提供了 QueryBySpecExecutor<T>
包含了許多查詢方法:
public interface QueryBySpecExecutor<T> {
List<T> findBySpec(Object spec);
List<T> findBySpec(Object spec, Sort sort);
Page<T> findBySpec(Object spec, Pageable pageable);
// … more functionality omitted.
}
只要在原本的 repository interface 中去繼承 QueryBySpecExecutor<T>
就可以直接使用了:
public interface PersonRepository
extends JpaRepository<Person, Long>, QueryBySpecExecutor<Person> {
...
}
@Service
public class PersonService {
@Autowired PersonRepository personRepository;
public List<Person> findPeople(PersonCriteria criteria) {
return personRepository.findBySpec(criteria);
}
}
Customize Base Repository
在配置的過程中, QBS 會自動配置 Spring Data JPA 的 Base Repository, 預設的實作為 DefaultQueryBySpecExecutor
由於 Java 只能單一繼承, 為了應用程式可以保留原有的 Parent Base Repository, QBS 還多提供了 QueryBySpecExecutorAdapter
擴展點
你的應用程式可以視情況選擇繼承 DefaultQueryBySpecExecutor
或實作 QueryBySpecExecutorAdapter
去客製化 Base Repository, 如:
class MyRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID>
implements QueryBySpecExecutorAdapter<T> {
@Setter
@Getter
private SpecMapper specMapper;
private final EntityManager entityManager;
MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);
// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}
@Override
public Class<T> getDomainClass() {
return super.getDomainClass();
}
@Transactional
public <S extends T> S mySave(S entity) {
// implementation goes here
}
}
並且透過 properties 中的 spec.mapper.repository-base-class
設定成自定義的 base repository 的 fulll package name, 如:
spec:
mapper:
repository-base-class: com.example.MyRepositoryImpl
2.2 - Config SpecMapper
Starter 會在 App 啟動的過程中自動的配置一個 Default SpecMapper, 並註冊成 Spring @Bean 中, 你可以透過 Autowired 的方式跟 Spring 取得.
@Autowired SpecMapper specMapper;
例如, 我想要在轉換成 Specification 後, 先做一些加強再去查詢, 則範例如下:
class PersonService {
@Autowired SpecMapper specMapper;
@Autowired PersonRepository personRepository;
List<Person> getPersonByCriteria(PersonCriteria criteria) {
var spec = specMapper.toSpec(criteria);
// Perform additional operations on the spec, ex:
// spec = spec.and((root, query, criteriaBuilder) -> {
// ...
// });
return personRepository.findAll(spec);
}
}
在上述範例中, SpecMapper
被注入到了 PersonService
, 開發人員就可以使用 specMapper.toSpec()
將 criteria 物件轉換成 specification, 接著就可以對 specification 做更多的調整, 最後才傳入 personRepository
查詢
Configuration
Starter 提供了許多方式讓開發人員調整 Default SpecMapper 的配置
SpecificationResolver
SpecificationResolver
是用來增加自定義的 Spec Annotation, 只要將你要增加的客製化實作註冊成 Spring @Bean, 在 App 啟動的過程中就會自動的偵測及配置
範例如下:
@Configuration
class MyConfig {
@Bean
SpecificationResolver myResolver() {
return ...
}
}
如果你的 SpecificationResolver
需要用到 SpecMapper
本身, 則你可以包裝成 SpecificationResolverCodecBuilder
, 在建構 resolver 時就會把 SpecCodec
, 即 SpecMapper
的 interface, 傳進去, 例如:
@Configuration
class MyConfig {
@Bean
SpecificationResolverCodecBuilder myResolver() {
return MySpecificationResolver::new;
}
}
class MySpecificationResolver implements SpecificationResolver {
private final SpecCodec codec;
MySpecificationResolver(SpecCodec codec) {
// Keep the SpecCodec around to used.
this.codec = codec;
}
// implementation goes here
}
SkippingStrategy
SkippingStrategy
用來提供欄位跳脫的策略, 只要將你自定義的實作註冊成 Spring @Bean, 在 App 啟動的過程中就會自動的偵測並加入到 Default SpecMapper 中!
配置範例如下:
@Configuration
class MyConfig {
@Bean
SkippingStrategy mySkippingStrategy() {
return ...
}
}
ASTWriterFactory
在 Logging 中提供了不同的 Logger Name 策略, 透過 properties 中的 spec.mapper.impersonate-logger
, 可以設定是否要偽裝成實際處理的 object logger, 預設是關閉的, 若要開啟範例如下:
spec:
mapper:
# 是否要偽裝成實際處理的 object logger, 預設關閉
impersonate-logger: true
若你需要完整的客製化, 只要將你自定義的 ASTWriterFactory
實作註冊成 Spring @Bean, 在 App 啟動的過程中就會自動的偵測並加入到 Default SpecMapper 中!
配置範例如下:
@Configuration
class MyConfig {
@Bean
ASTWriterFactory myASTWriterFactory() {
return ...
}
}
Customize SpecMapper
當然, 你也可以完全的客製化 SpecMapper
, 只要將你的 SpecMapper
註冊成 Spring @Bean, 就會最優先的使用!
配置範例如下:
@Configuration
class MyConfig {
@Bean
SpecMapper mySpecMapper() {
return SpecMapper.builder()
. ...
.build();
}
}