用於在 Spring Boot 中使用 JPA 建置和執行進階動態查詢的程式庫。
Map<String, String>
,支援帶有查詢參數的 GET 端點。透過jpa-search-helper,您的控制器* 將能夠接收以下請求:
模式1 :
curl -X GET
' https://myexampledomain.com/persons?
firstName=Biagio
&lastName_startsWith=Toz
&birthDate_gte=19910101
&country_in=IT,FR,DE
&address_eq=Via Roma 1,Via Milano/,1,20 West/,34th Street
&company.name_in=Microsoft,Apple,Google
&company.employees_between=500,5000 '
模式2 :
curl -X POST -H " Content-type: application/json " -d ' {
"filter" : {
"operator": "and", // the first filter must contain a root operator: AND, OR or NOT
"filters" : [
{
"operator": "eq",
"key": "firstName",
"value": "Biagio"
},
{
"operator": "or",
"filters": [
{
"operator": "startsWith",
"key": "lastName",
"value": "Toz",
"options": {
"ignoreCase": true
}
},
{
"operator": "endsWith",
"key": "lastName",
"value": "ZZI",
"options": {
"ignoreCase": true,
"trim" : true
}
}
]
},
{
"operator": "in",
"key": "company.name",
"values": ["Microsoft", "Apple", "Google"]
},
{
"operator": "or",
"filters": [
{
"operator": "gte",
"key": "birthDate",
"value": "19910101"
},
{
"operator": "lte",
"key": "birthDate",
"value": "20010101"
}
]
},
{
"operator": "between",
"key" : "company.employees",
"values": [500, 5000],
"options": {
"negate": true
}
}
]
},
"options": {
"pageSize": 10,
"pageOffset": 0,
"sortKey": "birthDate",
"sortDesc": false
}
} ' ' https://myexampledomain.com/persons '
..你是怎麼做到的?閱讀此自述文件!
*請注意:該庫不公開控制器/HTTP 端點,而僅提供將建置和執行查詢的儲存庫。
JPA 搜尋助手 | 春季啟動 | 爪哇 |
---|---|---|
[v0.0.1 - v2.1.1] | 3.2.x | [17 - 23] |
[v3.0.0 - v3.2.2] | 3.3.x | [17 - 23] |
[v3.3.0 - 最新] | 3.4.x | [17 - 23] |
<dependency>
<groupId>app.tozzi</groupId>
<artifactId>jpa-search-helper</artifactId>
<version>3.3.0</version>
</dependency>
implementation 'app.tozzi:jpa-search-helper:3.3.0
@Searchable
註解首先將@Searchable
註解套用到網域模型中的字段,或套用到 JPA 實體中您想要可供搜尋的欄位。如果您想要在其他物件中搜尋某些字段,請使用@NestedSearchable
註解這些字段。
@ Data
public class Person {
@ Searchable
private String firstName ;
@ Searchable
private String lastName ;
@ Searchable ( entityFieldKey = "dateOfBirth" )
private Date birthDate ;
@ Searchable
private String country ;
@ Searchable
private String fillerOne ;
@ Searchable
private String fillerTwo ;
@ NestedSearchable
private Company company ;
@ Data
public static class Company {
@ Searchable ( entityFieldKey = "companyEntity.name" )
private String name ;
@ Searchable ( entityFieldKey = "companyEntity.employeesCount" )
private int employees ;
}
}
該註釋允許您指定:
核心屬性:
entityFieldKey
:實體 bean 上定義的欄位名稱(如果在實體 bean 上使用註釋,則無需指定)。如果未指定,鍵將是欄位名稱。
targetType
:實體的託管物件類型。如果未指定,程式庫會嘗試根據欄位類型取得它(例如,沒有目標類型定義的整數欄位將為INTEGER
)。如果沒有與管理的類型相容的類型,它將作為字串進行管理。託管類型:
STRING
、 INTEGER
、 DOUBLE
、 FLOAT
、 LONG
BIGDECIMAL
、 BOOLEAN
十進位、布林型、 DATE
、 LOCALDATE
、本地LOCALDATETIME
、本地LOCALTIME
、 OFFSETDATETIME
日期時間、 OFFSETTIME
、 ZONEDDATETIME
。驗證屬性:
datePattern
:僅適用於DATE
、 LOCALDATE
、 LOCALDATETIME
、 LOCALTIME
、 OFFSETDATETIME
、 OFFSETTIME
、 ZONEDDATETIME
目標類型。定義要使用的日期模式。maxSize, minSize
:值的最大/最小長度。maxDigits, minDigits
:僅適用於數字類型。最大/最小位數。regexPattern
:正規表示式模式。decimalFormat
:僅適用於十進制數字類型。預設#.##
其他:
sortable
:如果為 false,則該欄位可以用於搜索,但不能用於排序。預設值:true。trim
:應用修剪。tags
:如果域模型字段可以對應多個實體字段,則很有用(此範例可在下方找到)。allowedFilters
:專門允許的過濾器。notAllowedFilters
:不允許的過濾器。likeFilters
:允許類似的過濾器( contains 、 startsWith 、 endsWith )。預設值:true。繼續這個例子,我們的實體類別:
@ Entity
@ Data
public class PersonEntity {
@ Id
private Long id ;
@ Column ( name = "FIRST_NAME" )
private String firstName ;
@ Column ( name = "LAST_NAME" )
private String lastName ;
@ Column ( name = "BIRTH_DATE" )
private Date dateOfBirth ;
@ Column ( name = "COUNTRY" )
private String country ;
@ Column ( name = "FIL_ONE" )
private String fillerOne ;
@ Column ( name = "FIL_TWO" )
private String fillerTwo ;
@ OneToOne
private CompanyEntity companyEntity ;
}
@ Entity
@ Data
public class CompanyEntity {
@ Id
private Long id ;
@ Column ( name = "NAME" )
private String name ;
@ Column ( name = "COUNT" )
private Integer employeesCount ;
}
JPASearchRepository
介面您的 Spring JPA 儲存庫必須擴充JPASearchRepository<YourEntityClass>
。
@ Repository
public interface PersonRepository extends JpaRepository < PersonEntity , Long >, JPASearchRepository < PersonEntity > {
}
在您的經理中,或在您的服務中,或您想要使用儲存庫的任何地方:
模式 1 :定義一個映射<filter_key#options, value> :
// ...
@ Autowired
private PersonRepository personRepository ;
public List < Person > advancedSearch () {
// Pure example, in real use case it is expected that these filters can be passed directly by the controller
Map < String , String > filters = new HashMap <>();
filters . put ( "firstName_eq" , "Biagio" );
filters . put ( "lastName_startsWith#i" , "Toz" ); // ignore case
filters . put ( "birthDate_gte" , "19910101" );
filters . put ( "country_in" , "IT,FR,DE" );
filters . put ( "company.name_eq#n" , "Bad Company" ); // negation
filters . put ( "company.employees_between" , "500,5000" );
filters . put ( "fillerOne_null#n" , "true" ); // not null
filters . put ( "fillerTwo_empty" , "true" ); // empty
// Without pagination
List < PersonEntity > fullSearch = personRepository . findAll ( filters , Person . class );
filters . put ( "birthDate_sort" : "ASC" ); // sorting key and sorting order
filters . put ( "_limit" , "10" ); // page size
filters . put ( "_offset" , "0" ); // page offset
// With pagination
Page < PersonEntity > sortedAndPaginatedSearch = personRepository . findAllWithPaginationAndSorting ( filters , Person . class );
// ...
}
// ...
模式 2 :您需要使用JPASearchInput
取代地圖,為簡單起見,此處顯示為 JSON 格式。
{
"filter" : {
"operator" : " and " , // the first filter must contain a root operator: AND, OR or NOT
"filters" : [
{
"operator" : " eq " ,
"key" : " firstName " ,
"value" : " Biagio "
},
{
"operator" : " or " ,
"filters" : [
{
"operator" : " startsWith " ,
"key" : " lastName " ,
"value" : " Toz " ,
"options" : {
"ignoreCase" : true
}
},
{
"operator" : " endsWith " ,
"key" : " lastName " ,
"value" : " ZZI " ,
"options" : {
"ignoreCase" : true ,
"trim" : true
}
}
]
},
{
"operator" : " in " ,
"key" : " company.name " ,
"values" : [ " Microsoft " , " Apple " , " Google " ]
},
{
"operator" : " or " ,
"filters" : [
{
"operator" : " gte " ,
"key" : " birthDate " ,
"value" : " 19910101 "
},
{
"operator" : " lte " ,
"key" : " birthDate " ,
"value" : " 20010101 "
}
]
},
{
"operator" : " empty " ,
"key" : " fillerOne " ,
"options" : {
"negate" : true
}
},
{
"operator" : " between " ,
"key" : " company.employees " ,
"values" : [ 500 , 5000 ],
"options" : {
"negate" : true
}
}
]
},
"options" : {
"pageSize" : 10 ,
"pageOffset" : 0 ,
"sortKey" : " birthDate " ,
"sortDesc" : false
}
}
透過模式 2,可以使用AND
、 OR
和NOT
來管理複雜的篩選器(見下文)。
InvalidFieldException
。InvalidValueException
。JPASearchException
過濾器名稱 | 圖書館鑰匙 | 支援的模式 |
---|---|---|
和 | 和 | 1, 2 |
或者 | 或者 | 2 |
不是 | 不是 | 2 |
透過模式 1 ,所有篩選器僅組成AND
搜尋。
若要使用其他運算子OR
和NOT
,您必須使用模式 2
過濾器名稱 | 圖書館鑰匙 | SQL | 支援的模式 | 所需值 |
---|---|---|---|---|
等於 | 情緒智商 | sql_col = 值 | 1,2 | 是的 |
包含 | 包含 | sql_col LIKE '%val%' | 1,2 | 是的 |
在 | 在 | sql_col IN (val1, val2, ..., valN) | 1,2 | 是的 |
開始於 | 開始於 | sql_col LIKE 'val%' | 1,2 | 是的 |
結束於 | 結束於 | sql_col LIKE '%val' | 1,2 | 是的 |
大於 | GT | sql_col > val | 1,2 | 是的 |
大於或等於 | 通用電氣 | sql_col >= 值 | 1,2 | 是的 |
少於 | 其 | sql_col < 值 | 1,2 | 是的 |
小於或等於 | LTE | sql_col <= val | 1,2 | 是的 |
之間 | 之間 | val1 和 val2 之間的 sql_col | 1,2 | 是的 |
無效的 | 無效的 | sql_col 為空 | 1,2 | 不 |
空的 | 空的 | sql_collection_col 為 NULL | 1,2 | 不 |
模式1
選項說明 | 圖書館鑰匙 |
---|---|
忽略大寫 | #我 |
否定 | #n |
修剪 | #t |
選項鍵必須附加到過濾器中;例如?firstName_eq#i=Biagio或?firstName_eq#i#n=Biagio
模式2
選項說明 | 庫密鑰(Java 屬性) |
---|---|
忽略大寫 | 忽略大寫 |
否定 | 否定 |
修剪 | 修剪 |
對於每個過濾器,可以定義options
{
// ...
{
"operator" : " eq " ,
"key" : " firstName " ,
"value" : " Biagio " ,
"options" : {
"ignoreCase" : true ,
"trim" : false ,
"negate" : true
}
}
// ...
}
Java物件:
@ Data
public static class JPASearchFilterOptions {
private boolean ignoreCase ;
private boolean trim ;
private boolean negate ;
}
過濾器名稱 | 鑰匙 | 固定值 |
---|---|---|
限制(頁面大小) | 限制 | |
偏移量(頁碼) | 抵銷 | |
種類 | 種類 | 上升、下降 |
模式1 :例如?
模式2 :值根options
:
{
"filter" : {
// ...
},
"options" : {
"sortKey" : " firstName " ,
"sortDesc" : true ,
"pageSize" : 10 ,
"pageOffset" : 1
}
}
Java物件:
@ Data
public static class JPASearchOptions {
private String sortKey ;
private Boolean sortDesc = false ;
private Integer pageSize ;
private Integer pageOffset ;
private List < String > selections ;
}
,
: 例如?myField_in=test1,test2 --> 要搜尋的值:[" test1 ", " test2 "]/,
: 例如?myField_in=test1,test2/,test3 --> 要搜尋的值:[" test1 ", " test2,test3 "] @Projectable
註解首先將@Projectable
註解套用至您想要可供選擇的網域模型或 JPA 實體中的欄位。如果您想要在其他物件中選擇某些字段,請使用@NestedProjectable
註解這些字段。
@ Data
public class Person {
@ Searchable
private String firstName ;
@ Projectable
@ Searchable
private String lastName ;
@ Projectable ( entityFieldKey = "dateOfBirth" )
@ Searchable ( entityFieldKey = "dateOfBirth" )
private Date birthDate ;
@ Searchable
private String country ;
@ Searchable
private String fillerOne ;
@ Searchable
private String fillerTwo ;
@ NestedProjectable
@ NestedSearchable
private Company company ;
@ Data
public static class Company {
@ Searchable ( entityFieldKey = "companyEntity.name" )
private String name ;
@ Projectable ( entityFieldKey = "companyEntity.employeesCount" )
@ Searchable ( entityFieldKey = "companyEntity.employeesCount" )
private int employees ;
}
}
該註釋允許您指定:
核心屬性:
entityFieldKey
:實體 bean 上定義的欄位名稱(如果在實體 bean 上使用註釋,則無需指定)。如果未指定,鍵將是欄位名稱。JPASearchRepository
介面您的 Spring JPA 儲存庫必須擴充JPAProjectionRepository<YourEntityClass>
。
@ Repository
public interface PersonRepository extends JpaRepository < PersonEntity , Long >, JPASearchRepository < PersonEntity >, JPAProjectionRepository < PersonEntity > {
}
在您的經理中,或在您的服務中,或您想要使用儲存庫的任何地方:
模式 1 :定義(或新增至用於模式 1 搜尋的地圖)地圖:
,
// ...
@ Autowired
private PersonRepository personRepository ;
public List < Person > advancedSearch () {
// Pure example, in real use case it is expected that these filters can be passed directly by the controller
Map < String , String > filters = new HashMap <>();
filters . put ( "firstName_eq" , "Biagio" );
filters . put ( "lastName_startsWith#i" , "Toz" ); // ignore case
filters . put ( "birthDate_gte" , "19910101" );
filters . put ( "country_in" , "IT,FR,DE" );
filters . put ( "company.name_eq#n" , "Bad Company" ); // negation
filters . put ( "company.employees_between" , "500,5000" );
filters . put ( "fillerOne_null#n" , "true" ); // not null
filters . put ( "fillerTwo_empty" , "true" ); // empty
// Selections
filters . put ( "selections" , "lastName,birthDate,company.employees" );
// Without sorting
List < Map < String , Object >> result = personRepository . projection ( filters , Person . class , PersonEntity . class );
filters . put ( "birthDate_sort" : "ASC" ); // sorting key and sorting order
// With sorting
List < Map < String , Object >> sortedAndPaginatedSearch = personRepository . projectionWithSorting ( filters , Person . class , PersonEntity . class );
// ... convert the list of maps into your model
}
// ...
模式 2 :您需要使用JPASearchInput
取代地圖,為簡單起見,此處顯示為 JSON 格式。
{
"filter" : {
"operator" : " and " , // the first filter must contain a root operator: AND, OR or NOT
"filters" : [
{
"operator" : " eq " ,
"key" : " firstName " ,
"value" : " Biagio "
},
{
"operator" : " or " ,
"filters" : [
{
"operator" : " startsWith " ,
"key" : " lastName " ,
"value" : " Toz " ,
"options" : {
"ignoreCase" : true
}
},
{
"operator" : " endsWith " ,
"key" : " lastName " ,
"value" : " ZZI " ,
"options" : {
"ignoreCase" : true ,
"trim" : true
}
}
]
},
{
"operator" : " in " ,
"key" : " company.name " ,
"values" : [ " Microsoft " , " Apple " , " Google " ]
},
{
"operator" : " or " ,
"filters" : [
{
"operator" : " gte " ,
"key" : " birthDate " ,
"value" : " 19910101 "
},
{
"operator" : " lte " ,
"key" : " birthDate " ,
"value" : " 20010101 "
}
]
},
{
"operator" : " empty " ,
"key" : " fillerOne " ,
"options" : {
"negate" : true
}
},
{
"operator" : " between " ,
"key" : " company.employees " ,
"values" : [ 500 , 5000 ],
"options" : {
"negate" : true
}
}
]
},
"options" : {
"pageSize" : 10 ,
"pageOffset" : 0 ,
"sortKey" : " birthDate " ,
"sortDesc" : false ,
"selections" : [
" lastName " ,
" birthDate " ,
" company.employees "
]
}
}
對於這兩種模式,投影將傳回 List<Map<String, Object>> 結果,其中映射結構和鍵將反映實體結構(需要明確toJson(entityList) == toJson(mapList) )
註1:
請注意:預設投影強制所有連接關係為 LEFT JOIN。如果您不希望出現此行為,請選擇使用儲存庫方法(具有「Classic」後綴的方法),該方法允許您僅修改要修改的關係
註2:
投影,無論您是否想要,都將始終提取代表實體(或相關實體)主鍵的字段
註3:
不支援分頁
InvalidFieldException
。JPASearchException
可以強制使用 fetch 進行聯接,以允許 Hibernate(或您的 JPA 框架)對實體上定義的關係執行單一查詢。這只有在沒有分頁的情況下才有可能:
// ...
Map < String , JoinFetch > fetches = Map . of ( "companyEntity" , JoinFetch . LEFT );
personRepository . findAll ( filters , Person . class , fetches );
// ...
如果你有一個領域模型是多個實體轉換的結果,那麼可以明確指定一個映射(字串,字串),其鍵代表領域模型欄位的名稱,值是領域模型欄位的名稱要搜尋的實體:
// ...
Map < String , String > entityFieldMap = Map . of ( "company" , "companyEntity.name" );
// Without pagination
personRepository . findAll ( filters , Person . class , fetches , entityFieldMap );
// With pagination
personRepository . findAllWithPaginationAndSorting ( filters , Person . class , entityFieldMap );
// ...
另一個特殊情況可能是一個物件可以在領域模型中重複以表示實體的多個部分。搜尋的解決方案:
@ Entity
public class CoupleEntity {
@ Id
private Long id ;
@ Column ( name = "p1_fn" )
private String p1FirstName ;
@ Column ( name = "p1_ln" )
private String p1LastName ;
@ Column ( name = "p2_fn" )
private String p2FirstName ;
@ Column ( name = "p2_ln" )
private String p2LastName ;
}
@ Data
public class Couple {
@ NestedSearchable
private Person p1 ;
@ NestedSearchable
private Person p2 ;
@ Data
public static class Person {
@ Searchable ( tags = {
@ Tag ( fieldKey = "p1.firstName" , entityFieldKey = "p1FirstName" ),
@ Tag ( fieldKey = "p2.firstName" , entityFieldKey = "p2FirstName" ),
})
private String firstName ;
@ Searchable ( tags = {
@ Tag ( fieldKey = "p1.lastName" , entityFieldKey = "p1LastName" ),
@ Tag ( fieldKey = "p2.lastName" , entityFieldKey = "p2LastName" ),
})
private String lastName ;
}
}
curl - request GET
- url ' https://www.myexampledomain.com/couples?
p1.firstName_iEq=Romeo
&p2.firstName_iEq=Giulietta '
請注意:該庫不公開任何端點,因此沒有控制器。此處提供了詳盡且完整的範例項目。
控制器:
@ RestController
@ RequestMapping ( "/persons" )
public class PersonController {
@ Autowired
private PersonManager personManager ;
@ GetMapping ( produces = MediaType . APPLICATION_JSON_VALUE )
public List < Person > findPersons ( @ RequestParam Map < String , String > requestParams ) {
return personManager . find ( requestParams );
}
@ GetMapping ( path = "/projection" , produces = MediaType . APPLICATION_JSON_VALUE )
public List < Person > projection ( @ RequestParam Map < String , String > requestParams ) {
return personManager . projection ( requestParams );
}
}
服務/管理員 bean:
@ Service
public class PersonManager {
@ Autowired
private PersonRepository personRepository ;
public List < Person > find ( Map < String , String > filters ) {
return personRepository . findAllWithPaginationAndSorting ( filters , Person . class ). stream (). map ( this :: toModel ). toList ();
}
public List < Person > projection ( Map < String , String > filters ) {
return personRepository . projection ( filters , Person . class , PersonEntity . class ). stream (). map ( this :: toModel ). toList ();
}
private static Person toModel ( PersonEntity personEntity ) {
// ...
}
private static Person toModel ( Map < String , Object > entityMap ) {
// ...
}
}
捲曲:
curl - X GET
' http://localhost:8080/persons?
firstName=Biagio
&lastName_startsWith=Toz
&birthDate_gte=19910101
&country_in=IT,FR,DE
&company.name_in=Microsoft,Apple
&company.employees_between=500,5000 '
或者
curl - X GET
' http://localhost:8080/persons/projection?
firstName=Biagio
&lastName_startsWith=Toz
&birthDate_gte=19910101
&country_in=IT,FR,DE
&company.name_in=Microsoft,Apple
&company.employees_between=500,5000
&selections=firstName,birthDate '
控制器:
@ RestController
@ RequestMapping ( "/persons" )
@ Validated
public class PersonController {
@ Autowired
private PersonManager personManager ;
@ PostMapping ( produces = MediaType . APPLICATION_JSON_VALUE , consumes = MediaType . APPLICATION_JSON_VALUE )
public List < Person > findPersons ( @ Valid @ RequestBody JPASearchInput input ) {
return personManager . find ( input );
}
}
@ PostMapping ( path = "/projection" , produces = MediaType . APPLICATION_JSON_VALUE , consumes = MediaType . APPLICATION_JSON_VALUE )
public List < Person > projection ( @ Valid @ RequestBody JPASearchInput input ) {
return personManager . projection ( input );
}
}
服務/管理員 bean:
@ Service
public class PersonManager {
@ Autowired
private PersonRepository personRepository ;
public List < Person > find ( JPASearchInput input ) {
return personRepository . findAllWithPaginationAndSorting ( input , Person . class ). stream (). map ( this :: toModel ). toList ();
}
public List < Person > find ( JPASearchInput input ) {
return personRepository . projection ( input , Person . class , PersonEntity . class ). stream (). map ( this :: toModel ). toList ();
}
private static Person toModel ( PersonEntity entity ) {
// ...
}
private static Person toModel ( Map < String , Object > entityMap ) {
// ...
}
}
捲曲:
curl -X POST -H " Content-type: application/json " -d ' {
"filter" : {
"operator": "and", // the first filter must contain a root operator: AND, OR or NOT
"filters" : [
{
"operator": "eq",
"key": "firstName",
"value": "Biagio"
},
{
"operator": "or",
"filters": [
{
"operator": "startsWith",
"key": "lastName",
"value": "Toz",
"options": {
"ignoreCase": true
}
},
{
"operator": "endsWith",
"key": "lastName",
"value": "ZZI",
"options": {
"ignoreCase": true,
"trim" : true
}
}
]
},
{
"operator": "in",
"key": "company.name",
"values": ["Microsoft", "Apple", "Google"]
},
{
"operator": "or",
"filters": [
{
"operator": "gte",
"key": "birthDate",
"value": "19910101"
},
{
"operator": "lte",
"key": "birthDate",
"value": "20010101"
}
]
},
{
"operator": "between",
"key" : "company.employees",
"values": [500, 5000],
"options": {
"negate": true
}
}
]
},
"options": {
"pageSize": 10,
"pageOffset": 0,
"sortKey": "birthDate",
"sortDesc": false
}
} ' ' http://localhost:8080/persons '
或者
curl -X POST -H " Content-type: application/json " -d ' {
"filter" : {
"operator": "and", // the first filter must contain a root operator: AND, OR or NOT
"filters" : [
{
"operator": "eq",
"key": "firstName",
"value": "Biagio"
},
{
"operator": "or",
"filters": [
{
"operator": "startsWith",
"key": "lastName",
"value": "Toz",
"options": {
"ignoreCase": true
}
},
{
"operator": "endsWith",
"key": "lastName",
"value": "ZZI",
"options": {
"ignoreCase": true,
"trim" : true
}
}
]
},
{
"operator": "in",
"key": "company.name",
"values": ["Microsoft", "Apple", "Google"]
},
{
"operator": "or",
"filters": [
{
"operator": "gte",
"key": "birthDate",
"value": "19910101"
},
{
"operator": "lte",
"key": "birthDate",
"value": "20010101"
}
]
},
{
"operator": "between",
"key" : "company.employees",
"values": [500, 5000],
"options": {
"negate": true
}
}
]
},
"options": {
"sortKey": "birthDate",
"sortDesc": false,
"selections" : [
"birthDate",
"firstName",
"lastName"
]
}
} ' ' http://localhost:8080/persons/projection '