{"id":25831124,"url":"https://github.com/biagiot/jpa-search-helper","last_synced_at":"2026-09-06T01:30:29.662Z","repository":{"id":219474324,"uuid":"749112680","full_name":"biagioT/jpa-search-helper","owner":"biagioT","description":"Helper library for building and running advanced and dynamic queries using JPA in Spring Boot.","archived":false,"fork":false,"pushed_at":"2024-10-16T17:23:07.000Z","size":243,"stargazers_count":36,"open_issues_count":2,"forks_count":6,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-18T12:17:29.408Z","etag":null,"topics":["advanced-search","dynamic-queries","dynamic-query","hibernate","hibernate-jpa","java","jpa","persistence-api","query","query-builder","query-engine","rdbms","repository","search","search-engine","spring","springboot","sql","sql-builder"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/biagioT.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-27T16:08:28.000Z","updated_at":"2024-10-17T05:04:09.000Z","dependencies_parsed_at":"2024-02-08T19:24:00.374Z","dependency_job_id":"52e49b01-b785-4e70-9b6f-e4fea3f0bbf8","html_url":"https://github.com/biagioT/jpa-search-helper","commit_stats":null,"previous_names":["biagiot/jpa-search-helper"],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biagioT%2Fjpa-search-helper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biagioT%2Fjpa-search-helper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biagioT%2Fjpa-search-helper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/biagioT%2Fjpa-search-helper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/biagioT","download_url":"https://codeload.github.com/biagioT/jpa-search-helper/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241225636,"owners_count":19930236,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["advanced-search","dynamic-queries","dynamic-query","hibernate","hibernate-jpa","java","jpa","persistence-api","query","query-builder","query-engine","rdbms","repository","search","search-engine","spring","springboot","sql","sql-builder"],"created_at":"2025-02-28T20:19:47.154Z","updated_at":"2026-09-06T01:30:29.626Z","avatar_url":"https://github.com/biagioT.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# JPA Search Helper\n\nLibrary for building and running advanced and dynamic queries using JPA in Spring Boot.\n\n## Why use JPA Search Helper?\n\nBuilding dynamic and type-safe queries with JPA can quickly become complex and repetitive.  \n`JPA Search Helper` simplifies this process by providing a fluent, declarative way to build advanced search filters and projections, with minimal boilerplate.\n\nWith this library, you can:\n- Dynamically build queries using simple request parameters or structured JSON input.\n- Combine filters with logical operators (`AND`, `OR`, `NOT`), without writing custom `Specification` or `CriteriaBuilder` code.\n- Reuse your existing JPA entities or domain models through simple annotations.\n- Support projections to return only specific fields instead of entire entities.\n- Keep full control of the repository layer, without exposing new endpoints.\n\nIn short, it helps you build **powerful, flexible search APIs** in Spring Boot, **with zero query code**.\n\n\n## Status\n\n| Type | Status |\n| ---- | ------ |\n| Build (CI) | [![Build (github)](https://github.com/biagioT/jpa-search-helper/actions/workflows/ci.yml/badge.svg)](https://github.com/biagioT/jpa-search-helper/actions/workflows/ci.yml) |\n| CodeQL | [![Build (github)](https://github.com/biagioT/jpa-search-helper/actions/workflows/codeql.yml/badge.svg)](https://github.com/biagioT/jpa-search-helper/actions/workflows/codeql.yml) |\n| Artifact | [![Maven Central](https://img.shields.io/maven-central/v/app.tozzi/jpa-search-helper.svg)](https://mvnrepository.com/artifact/app.tozzi/jpa-search-helper) |\n| Javadocs | [![javadoc](https://javadoc.io/badge2/app.tozzi/jpa-search-helper/javadoc.svg)](https://javadoc.io/doc/app.tozzi/jpa-search-helper) |\n| OpenSSF Score | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/biagioT/jpa-search-helper/badge)](https://securityscorecards.dev/viewer/?uri=github.com/biagioT/jpa-search-helper) |\n| OpenSSF Best Practices | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/11512/badge)](https://www.bestpractices.dev/projects/11512) |\n| Dependabot | [![Dependabot](https://img.shields.io/badge/Dependabot-enabled-brightgreen?logo=dependabot\u0026labelColor=555555)](https://github.com/biagioT/jpa-search-helper/security/dependabot) |\n\n## TL;DR\n### Key features\n- **Queries**: the library supports two modes for building advanced and dynamic queries:\n  -  *Mode 1*: Via `Map\u003cString, String\u003e`, to support GET endpoints with query params.\n  -  *Mode 2*: Via an [object](https://github.com/biagioT/jpa-search-helper/blob/main/src/main/java/app/tozzi/model/input/JPASearchInput.java), to support POST endpoints that expect query parameters in the body (from *2.0.0* version)\n- **Projection**:  for both modes, the library allows you to extract only a subselection of fields from the query (from *3.2.0* version)\n\n### Spoiler\nWith **jpa-search-helper** your controller* can handle requests like this:\n\n*Mode 1*:\n```bash  \ncurl -X GET \\\n  'https://myexampledomain.com/persons?\n  firstName=Biagio\n  \u0026lastName_startsWith=Toz\n  \u0026birthDate_gte=19910101\n  \u0026country_in=IT,FR,DE\n  \u0026address_eq=Via Roma 1,Via Milano/,1,20 West/,34th Street\n  \u0026company.name_in=Microsoft,Apple,Google\n  \u0026company.employees_between=500,5000'\n```  \n\n*Mode 2*:\n```bash  \ncurl -X POST -H \"Content-type: application/json\" -d '{\n  \"filter\" : {\n      \"operator\": \"and\", // the first filter must contain a root operator: AND, OR or NOT\n      \"filters\" : [\n        {\n          \"operator\": \"eq\",\n          \"key\": \"firstName\",\n          \"value\": \"Biagio\"\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"startsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"Toz\",\n              \"options\": {\n                \"ignoreCase\": true\n              }\n            },\n            {\n              \"operator\": \"endsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"ZZI\",\n              \"options\": {\n                \"ignoreCase\": true,\n                \"trim\" : true\n              }\n            }\n          ]\n        },\n        {\n          \"operator\": \"in\",\n          \"key\": \"company.name\",\n          \"values\": [\"Microsoft\", \"Apple\", \"Google\"]\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"gte\",\n              \"key\": \"birthDate\",\n              \"value\": \"19910101\"\n            },\n            {\n              \"operator\": \"lte\",\n              \"key\": \"birthDate\",\n              \"value\": \"20010101\"\n            }\n          ]\n        },\n        {\n          \"operator\": \"between\",\n          \"key\" : \"company.employees\",\n          \"values\": [500, 5000],\n          \"options\": {\n            \"negate\": true\n          }\n        }\n      ]\n  },\n  \"options\": {\n    \"pageSize\": 10,\n    \"pageOffset\": 0,\n    \"sortOptions\" : [\n      {\n          \"key\": \"birthDate\",\n          \"desc\": false\n      }\n    ]\n  }\n  \n}' 'https://myexampledomain.com/persons'\n```  \n\n..how can you do it? Keep reading this README!\n\n**\\*** Please note: the library itself does not expose any controllers or HTTP endpoints, it only provides the repository layer responsible for building and executing the queries.\n\n## Compatibility Matrix\n\n#### Minimum requirements\n- Java 17 or later\n- Spring Boot 3.2.x or later\n\n| JPA Search Helper | Spring Boot | JDK       |  \n|-------------------|-------------|-----------|  \n| [v0.0.1 - v2.1.1] | 3.2.x       | [17 - 25] |  \n| [v3.0.0 - v3.2.2] | 3.3.x       | [17 - 25] |  \n| [v3.3.0 - v3.4.5] | 3.4.x       | [17 - 25] |  \n| [v3.5.0 - v3.5.5] | 3.5.x       | [17 - 25] |  \n| [v3.6.0 - v3.6.4] | 4.x.x       | [17 - 25] |  \n| [v4.0.0 - latest] | 4.x.x       | [17 - 25] |  \n\n## Project dependency\n#### Maven\n```  \n\u003cdependency\u003e  \n \u003cgroupId\u003eapp.tozzi\u003c/groupId\u003e \n \u003cartifactId\u003ejpa-search-helper\u003c/artifactId\u003e \n \u003cversion\u003e4.1.0\u003c/version\u003e\n\u003c/dependency\u003e  \n```  \n\n#### Gradle\n```  \nimplementation 'app.tozzi:jpa-search-helper:4.1.0'\n```\n\n## Queries - Usage\n### 1. `@Searchable` annotation\nStart by applying the `@Searchable` annotation to the fields in your Domain Model, or alternatively your JPA entity, **that you want to make available for search**.\nIf you have fields that you want to make searchable within other objects then annotate these with `@NestedSearchable`.\n\n```java\n@Data\npublic class Person {\n\n  @Searchable\n  private String firstName;\n\n  @Searchable\n  private String lastName;\n\n  @Searchable(entityFieldKey = \"dateOfBirth\")\n  private Date birthDate;\n\n  @Searchable\n  private String country;\n  \n  @Searchable\n  private String fillerOne;\n  \n  @Searchable\n  private String fillerTwo;\n\n  @NestedSearchable\n  private Company company;\n\n  @Searchable(targetType = JPASearchType.JSONB, entityFieldKey = \"data\", jsonPath = \"address.street\")\n  private String addressStreet;\n\n  @Searchable(targetType = JPASearchType.JSONB, entityFieldKey = \"data\", jsonPath = \"address.city\")\n  private String addressCity;\n  \n  @Data\n  public static class Company {\n\n    @Searchable(entityFieldKey= \"companyEntity.name\")\n    private String name;\n\n    @Searchable(entityFieldKey= \"companyEntity.employeesCount\")\n    private int employees;\n  }\n}\n```\nThe annotation allows you to specify:\n\n- Core properties:\n\n  - `entityFieldKey`:\n    the name of the field defined on the entity bean (not to be specified if using the annotation on the entity bean). If not specified the key will be the field name.\n  - `targetType`: the managed object type by entity. If not specified the librariy tries to obtain it based on field type (es. Integer field without target type definition will be `INTEGER`). If there is no type compatible with those managed, it will be managed as a string. Managed types:\n\n    - `STRING`, `INTEGER`, `DOUBLE`, `FLOAT`, `LONG`, `BIGDECIMAL`, `BOOLEAN`, `DATE`, `LOCALDATE`, `LOCALDATETIME`, `LOCALTIME`, `OFFSETDATETIME`, `OFFSETTIME`, `ZONEDDATETIME`, `ENUM`, `DATE_SQL`, `TIME_SQL`, `INSTANT`, `TIMESTAMP`, `UUID`, `JSONB`\n\n- Validation properties:\n\n  - `datePattern`: only for `DATE`, `LOCALDATE`, `LOCALDATETIME`, `LOCALTIME`, `OFFSETDATETIME`, `OFFSETTIME`, `ZONEDDATETIME`, `TIMESTAMP`, `TIME_SQL`, `DATE_SQL`, `INSTANT` target types. Defines the date pattern to use.\n  - `maxSize, minSize`: maximum/minimum length of the value (for `STRING` types).\n  - `maxDigits, minDigits`: only for numeric types. Maximum/minimum number of digits.\n  - `regexPattern`: regex pattern.\n  - `decimalFormat`: only for decimal numeric types. Default `#.##`\n\n- Other:\n  - `sortable`: if false, the field can be used by search but cannot be used for sorting. Default: true.\n  - `trim`: apply trim.\n  - `tags`: useful if the Domain Model field can correspond to multiple entity fields (the example is available further down).\n  - `allowedFilters`: exclusively allowed filters.\n  - `notAllowedFilters`: not allowed filters.\n  - `likeFilters`: allowed like filters (_contains_, _startsWith_, _endsWith_). Default: true.\n  - `ordinalEnum`: only for `ENUM` type; true if search via ordinal\n  - `jsonPath`: required for `JSONB`; represents the JSON path inside the jsonb column.\n  - `elementCollection`: set to true when the underlying entity attribute is annotated with `@ElementCollection` (e.g. `List\u003cString\u003e`). `EQ` and `IN` are evaluated with `cb.isMember(...)`; other operators fall back to a LEFT JOIN on the collection table.\n\nContinuing the example, here are our entity classes:\n\n```java\n@Entity\n@Data\npublic class PersonEntity {\n\n  @Id\n  private Long id;\n\n  @Column(name = \"FIRST_NAME\")\n  private String firstName;\n\n  @Column(name = \"LAST_NAME\")\n  private String lastName;\n\n  @Column(name = \"BIRTH_DATE\")\n  private Date dateOfBirth;\n\n  @Column(name = \"COUNTRY\")\n  private String country;\n\n  @Column(name = \"FIL_ONE\")\n  private String fillerOne;\n\n  @Column(name = \"FIL_TWO\")\n  private String fillerTwo;\n\n  @Type(JsonType.class)\n  @Column(name = \"DATA_ADDRESS\", columnDefinition = \"jsonb\")\n  private Map\u003cString, Object\u003e data;\n\n  @OneToOne\n  private CompanyEntity companyEntity;\n\n}\n\n@Entity\n@Data\npublic class CompanyEntity {\n\n  @Id\n  private Long id;\n\n  @Column(name = \"NAME\")\n  private String name;\n\n  @Column(name = \"COUNT\")\n  private Integer employeesCount;\n\n}\n```\n\n### 2. `JPASearchRepository` interface\nYour Spring JPA repository must extend `JPASearchRepository\u003cYourEntityClass\u003e`.\n\n```java\n@Repository\npublic interface PersonRepository extends JpaRepository\u003cPersonEntity, Long\u003e, JPASearchRepository\u003cPersonEntity\u003e {\n\n}\n```\n\n### 3. Search implementation\nIn your manager, or in your service, or wherever you want to use the repository:\n\n**Mode 1**: define a map _\u003cfilter_key#options, value\u003e_:\n\n```java\n// ...\n\n  @Autowired\n  private PersonRepository personRepository;\n\n  public List\u003cPerson\u003e advancedSearch() {\n  \n\t// This is a simple example; in a real use case, these filters would typically be passed directly from the controller.\n    Map\u003cString, String\u003e filters = new HashMap\u003c\u003e();\n    filters.put(\"firstName_eq\", \"Biagio\");\n    filters.put(\"lastName_startsWith#i\", \"Toz\"); // ignore case\n    filters.put(\"birthDate_gte\", \"19910101\"); \n    filters.put(\"country_in\", \"IT,FR,DE\");\n    filters.put(\"company.name_eq#n\", \"Bad Company\"); // negation\n    filters.put(\"company.employees_between\", \"500,5000\");\n    filters.put(\"fillerOne_null#n\", \"true\"); // not null\n    filters.put(\"fillerTwo_empty\", \"true\"); // empty\n    \n    // Without pagination\n    List\u003cPersonEntity\u003e fullSearch = personRepository.findAll(filters, Person.class);\n  \n    filters.put(\"birthDate_sort\", \"ASC\"); // sorting key and sorting order\n    filters.put(\"_limit\", \"10\"); // page size\n    filters.put(\"_offset\", \"0\"); // page offset\n    \n    // With pagination\n    Page\u003cPersonEntity\u003e sortedAndPaginatedSearch = personRepository.findAllWithPaginationAndSorting(filters, Person.class);\n    \n    // ...\n  }\n\n// ...\n```\n\n**Mode 2**: instead of a map, you will need to use [`JPASearchInput`](https://github.com/biagioT/jpa-search-helper/blob/main/src/main/java/app/tozzi/model/input/JPASearchInput.java), shown here, for simplicity, in JSON format.\n\n```json\n{\n  \"filter\" : {\n      \"operator\": \"and\", // the first filter must contain a root operator: AND, OR or NOT\n      \"filters\" : [\n        {\n          \"operator\": \"eq\",\n          \"key\": \"firstName\",\n          \"value\": \"Biagio\"\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"startsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"Toz\",\n              \"options\": {\n                \"ignoreCase\": true\n              }\n            },\n            {\n              \"operator\": \"endsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"ZZI\",\n              \"options\": {\n                \"ignoreCase\": true,\n                \"trim\" : true\n              }\n            }\n          ]\n        },\n        {\n          \"operator\": \"in\",\n          \"key\": \"company.name\",\n          \"values\": [\"Microsoft\", \"Apple\", \"Google\"]\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"gte\",\n              \"key\": \"birthDate\",\n              \"value\": \"19910101\"\n            },\n            {\n              \"operator\": \"lte\",\n              \"key\": \"birthDate\",\n              \"value\": \"20010101\"\n            }\n          ]\n        },\n        {\n          \"operator\": \"empty\",\n          \"key\": \"fillerOne\",\n          \"options\": {\n            \"negate\": true\n          }\n        },\n        {\n          \"operator\": \"between\",\n          \"key\" : \"company.employees\",\n          \"values\": [500, 5000],\n          \"options\": {\n            \"negate\": true\n          }\n        }\n      ]\n  },\n  \"options\": {\n    \"pageSize\": 10,\n    \"pageOffset\": 0,\n    \"sortOptions\" : [\n      {\n        \"key\": \"birthDate\",\n        \"desc\": false\n      }\n    ]\n  }\n  \n}\n```\nThrough *Mode 2* it is possible to manage complex filters with `AND`, `OR` and `NOT` (see later).\n\n#### Exceptions:\n- If a field does not exist, is not searchable or is not sortable, you will receive an `InvalidFieldException`.\n- If the value of a field does not meet the requirements you will receive an `InvalidValueException`.\n- Other cases: `JPASearchException`\n\n## Queries - Docs\n### Managed root operators\n| Filter name | Library Key | Supported modes |\n|-------------|-------------|-----------------|\n| AND         | and         | 1, 2            |\n| OR          | or          | 2               |\n| NOT         | not         | 2               |\n\nThrough *Mode 1*, all filters compose exclusively an `AND` search.\n\nTo use the other operators, `OR` and `NOT`, you must use *Mode 2*\n\n### Managed search filter operators\n| Filter name               | Library Key | SQL                                | Supported modes | Value required |\n|---------------------------|-------------|------------------------------------|-----------------|----------------|\n| Equals                    | eq          | sql_col = val                      | 1,2 | yes            |\n| Contains                  | contains    | sql_col LIKE '%val%'               | 1,2 | yes            |\n| In                        | in          | sql_col IN (val1, val2, ..., valN) | 1,2 | yes            |\n| Starts With               | startsWith  | sql_col LIKE 'val%'                | 1,2 | yes            |\n| Ends With                 | endsWith    | sql_col LIKE '%val'                | 1,2 | yes            |\n| Greater Than              | gt          | sql_col \u003e val                      | 1,2 | yes            |\n| Greater Than or Equal     | gte         | sql_col \u003e= val                     | 1,2 | yes            |\n| Less Than                 | lt          | sql_col \u003c val                      | 1,2 | yes            |\n| Less Than or Equal        | lte         | sql_col \u003c= val                     | 1,2 | yes            |\n| Between                   | between     | sql_col BETWEEN val1 AND val2      | 1,2 | yes            |\n| Null                      | null        | sql_col IS NULL                    | 1,2 | no             |\n| Empty                     | empty       | sql_collection_col IS EMPTY        | 1,2 | no             |\n\n#### Options\n**Mode 1**\n| Option description | Library Key |\n|--------------------|-------------|\n| Ignore case        | #i          |\n| Negation           | #n          |\n| Trim               | #t          |\n\nThe option keys must be appended to the filter; e.g. _?firstName_eq#i=Biagio_ or _?firstName_eq#i#n=Biagio_\n\n**Mode 2**\n| Option description | Library Key (Java attributes) |\n|------------------|-------------------------------|\n| Ignore case      | ignoreCase                    |\n| Negation      | negate                        |\n| Trim | trim                          |\n\nFor each filter it is possible to define `options`\n```json\n{\n  // ...\n      {\n         \"operator\": \"eq\",\n         \"key\": \"firstName\",\n         \"value\": \"Biagio\",\n         \"options\": {\n           \"ignoreCase\": true,\n           \"trim\": false,\n           \"negate\": true\n         }\n      }\n  // ...\n}\n```\nJava object:\n```java  \n@Data  \npublic static class JPASearchFilterOptions {  \n private boolean ignoreCase;  \n private boolean trim;  \n private boolean negate;  \n}  \n```\n\n### Pagination\n\n| Filter name | Key |\n|--|--|\n| Limit (page size) | _limit |\n| Offset (page number) | _offset |\n| Sort | \u003cfield\u003e_sort (values: ASC, DESC) |\n\n**Mode 1**: e.g. *?firstName_sort=DESC\u0026_limit=10\u0026_offset=0*\n\n**Mode 2**: value root `options`:\n```json\n{\n  \"filter\" : {\n    // ...\n  },\n  \"options\" : {\n    \"sortOptions\" : [\n      {\n        \"key\": \"firstName\",\n        \"desc\": true\n      }\n    ],\n    \"pageSize\": 10,\n    \"pageOffset\": 1\n  }\n}\n```\nJava object:\n```java\n@Data\npublic static class JPASearchOptions {\n    private List\u003cJPASortOptions\u003e sortOptions;\n    private Integer pageSize;\n    private Integer pageOffset;\n    private List\u003cString\u003e selections;\n}\n\n@Data\npublic static class JPASortOptions {\n\n  @NotEmpty\n  @NotNull\n  private String key;\n\n  private Boolean desc = false;\n}\n```\n\n### Other (only for Mode 1)\n- Separator for array values: `,`: e.g. _?myField_in=test1,test2_ --\u003e values to search for: [\"**test1**\", \"**test2**\"]\n- To escape separator: prepend `/` to the comma. Example: `?myField_in=test1,test2/,test3` --\u003e values to search for: [\"**test1**\", \"**test2,test3**\"]. So `/` is the escape character and `/,` is the escape sequence that represents a literal comma inside a value.\n\n## Projection - Usage\n\n### 1. `@Projectable` annotation\nStart by applying the `@Projectable` annotation to the fields in your Domain Model, or alternatively your JPA entity, **that you want to make available for selection**.\nIf you have fields that you want to make selectable within other objects then annotate these with `@NestedProjectable`.\n\n```java\n@Data\npublic class Person {\n\n  @Searchable\n  private String firstName;\n\n  @Projectable\n  @Searchable\n  private String lastName;\n\n  @Projectable(entityFieldKey = \"dateOfBirth\")\n  @Searchable(entityFieldKey = \"dateOfBirth\")\n  private Date birthDate;\n\n  @Searchable\n  private String country;\n  \n  @Searchable\n  private String fillerOne;\n  \n  @Searchable\n  private String fillerTwo;\n\n  @NestedProjectable\n  @NestedSearchable\n  private Company company;\n\n  @Data\n  public static class Company {\n\n    @Searchable(entityFieldKey= \"companyEntity.name\")\n    private String name;\n\t\n\t@Projectable(entityFieldKey= \"companyEntity.employeesCount\")\n    @Searchable(entityFieldKey= \"companyEntity.employeesCount\")\n    private int employees;\n  }\n}\n```\nThe annotation allows you to specify:\n\n- Core properties:\n\n  - `entityFieldKey`:\n    the name of the field defined on the entity bean (not to be specified if using the annotation on the entity bean). If not specified the key will be the field name.\n\n### 2. `JPASearchRepository` interface\nYour Spring JPA repository must extend `JPAProjectionRepository\u003cYourEntityClass\u003e`.\n\n```java  \n@Repository  \npublic interface PersonRepository extends JpaRepository\u003cPersonEntity, Long\u003e, JPASearchRepository\u003cPersonEntity\u003e, JPAProjectionRepository\u003cPersonEntity\u003e {  \n  \n}  \n```\n\u003e **ℹ️ Configuration Note (Spring Boot):**\n\u003e Starting from version `4.0.0`, the library ships a Spring Boot auto-configuration that registers `JPAProjectionRepositoryImpl` automatically. No additional `@ComponentScan` / `@EnableJpaRepositories` setup is required for Spring Boot consumers.\n\u003e\n\u003e If your application **does not** use Spring Boot auto-configuration, or you have customized your component scan, you may still need to add `app.tozzi` to your component scan to ensure the implementation bean is discovered:\n\n```java  \n@Configuration\n// Include your package AND the library package (only needed when NOT using Spring Boot auto-configuration)\n@ComponentScan(basePackages = {\"com.my-package\", \"app.tozzi\"})\n@EnableJpaRepositories(basePackages = {\"com.my-package.repository\"})\npublic class ApplicationConfiguration {\n    // ...\n}\n```\n\n### 3. Projection implementation\nIn your manager, or in your service, or wherever you want to use the repository:\n\n**Mode 1**: define (or add to the map used for the Mode 1 search) a map:\n\n- key: *selections*\n- value: the only fields that you want to extract separated by commas `,`\n\n```java\n// ...\n\n  @Autowired\n  private PersonRepository personRepository;\n\n  public List\u003cPerson\u003e advancedSearch() {\n  \n    // Pure example, in real use case it is expected that these filters can be passed directly by the controller\n    Map\u003cString, String\u003e filters = new HashMap\u003c\u003e();\n    filters.put(\"firstName_eq\", \"Biagio\");\n    filters.put(\"lastName_startsWith#i\", \"Toz\"); // ignore case\n    filters.put(\"birthDate_gte\", \"19910101\"); \n    filters.put(\"country_in\", \"IT,FR,DE\");\n    filters.put(\"company.name_eq#n\", \"Bad Company\"); // negation\n    filters.put(\"company.employees_between\", \"500,5000\");\n    filters.put(\"fillerOne_null#n\", \"true\"); // not null\n    filters.put(\"fillerTwo_empty\", \"true\"); // empty\n\n    // Selections\n    filters.put(\"selections\", \"lastName,birthDate,company.employees\");\n    \n    // Without sorting\n    List\u003cMap\u003cString, Object\u003e\u003e result = personRepository.projection(filters, Person.class, PersonEntity.class);\n  \n    filters.put(\"birthDate_sort\", \"ASC\"); // sorting key and sorting order\n    \n    // With sorting\n    List\u003cMap\u003cString, Object\u003e\u003e sortedAndPaginatedSearch = personRepository.projectionWithSorting(filters, Person.class, PersonEntity.class);\n    \n    // ... convert the list of maps into your model\n  }\n\n// ...\n```\n\n**Mode 2**: instead of a map, you will need to use [`JPASearchInput`](https://github.com/biagioT/jpa-search-helper/blob/main/src/main/java/app/tozzi/model/input/JPASearchInput.java), shown here, for simplicity, in JSON format.\n```json\n{\n  \"filter\" : {\n      \"operator\": \"and\", // the first filter must contain a root operator: AND, OR or NOT\n      \"filters\" : [\n        {\n          \"operator\": \"eq\",\n          \"key\": \"firstName\",\n          \"value\": \"Biagio\"\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"startsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"Toz\",\n              \"options\": {\n                \"ignoreCase\": true\n              }\n            },\n            {\n              \"operator\": \"endsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"ZZI\",\n              \"options\": {\n                \"ignoreCase\": true,\n                \"trim\" : true\n              }\n            }\n          ]\n        },\n        {\n          \"operator\": \"in\",\n          \"key\": \"company.name\",\n          \"values\": [\"Microsoft\", \"Apple\", \"Google\"]\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"gte\",\n              \"key\": \"birthDate\",\n              \"value\": \"19910101\"\n            },\n            {\n              \"operator\": \"lte\",\n              \"key\": \"birthDate\",\n              \"value\": \"20010101\"\n            }\n          ]\n        },\n        {\n          \"operator\": \"empty\",\n          \"key\": \"fillerOne\",\n          \"options\": {\n            \"negate\": true\n          }\n        },\n        {\n          \"operator\": \"between\",\n          \"key\" : \"company.employees\",\n          \"values\": [500, 5000],\n          \"options\": {\n            \"negate\": true\n          }\n        }\n      ]\n  },\n  \"options\": {\n    \"pageSize\": 10,\n    \"pageOffset\": 0,\n    \"sortOptions\" : [\n      {\n        \"key\": \"birthDate\",\n        \"desc\": false\n      }\n    ],\n    \"selections\" : [\n\t\t\"lastName\",\n\t\t\"birthDate\",\n\t\t\"company.employees\"\n\t]\n  }\n}\n```\n\nFor both modes, the projection will return a List\u003cMap\u003cString, Object\u003e\u003e result where **the map structure and keys will reflect the entity structure** (to be clear *toJson(entityList) == toJson(mapList)*)\n\nNote 1:\n\u003e By default, projection forces all Join relationships to use LEFT JOIN. If you don't want this behavior, choose to use the repository methods (methods with 'Classic' suffix) that allow you to possibly modify only the relations you want to modify\n\nNote 2:\n\u003e Projection, regardless of whether you want it or not, will always extract the fields that represent the primary keys of an entity (or related entities)\n\nNote 3:\n\n\u003e Pagination is not supported\n\n#### Exceptions:\n- If a field does not exist, is not searchable, is not sortable or is not projectable, you will receive an `InvalidFieldException`.\n- Other cases: `JPASearchException`\n\n---\n\n## Advanced Settings\n\n\u003e Starting from version `4.0.0`, the optional parameters (`fetches`, `entityFieldMap`, override join types, ...) are passed via the parameter objects `JPASearchOptions` (search) and `JPAProjectionOptions` (projection), instead of the many overloaded method signatures used in releases up to `3.6.x`. Build them with the fluent `builder()` or use the static `empty()` helper when no extra options are needed.\n\n### Join Fetch\nIt is possible to force joins with fetch to allow Hibernate (or your JPA framework) to execute a single query for the relationships defined on the entity. **This is only possible without pagination**:\n```java\n// ...\n\nvar options = JPASearchOptions.builder()\n        .fetches(Map.of(\"companyEntity\", JoinType.LEFT))\n        .build();\n\npersonRepository.findAll(filters, Person.class, options);\n\n// ...\n```\n\n### Multiple entities for the same Domain Model\nIf you have a Domain Model that is the result of the conversion of multiple entities, it is possible to explicitly specify a map (string, string) whose key represents the name of the Domain Model field and the value is the name of the field of the entity to be searched for:\n```java\n// ...\n\nvar options = JPASearchOptions.builder()\n        .fetches(Map.of(\"companyEntity\", JoinType.LEFT))\n        .entityFieldMap(Map.of(\"company\", \"companyEntity.name\"))\n        .build();\n\n// Without pagination\npersonRepository.findAll(filters, Person.class, options);\n\n// With pagination\npersonRepository.findAllWithPaginationAndSorting(filters, Person.class, options);\n\n// ...\n```\n\n### Multiple object for the same entity\nAnother special case could be where an object can be repeated within the Domain Model to represent multiple pieces of the entity. The solution for the search:\n```java\n\n@Entity\npublic class CoupleEntity {\n\n  @Id\n  private Long id;\n\n  @Column(name = \"p1_fn\")\n  private String p1FirstName;\n\n  @Column(name = \"p1_ln\")\n  private String p1LastName;\n\n  @Column(name = \"p2_fn\")\n  private String p2FirstName;\n\n  @Column(name = \"p2_ln\")\n  private String p2LastName;\n}\n\n@Data\npublic class Couple {\n\n  @NestedSearchable\n  private Person p1;\n\n  @NestedSearchable\n  private Person p2;\n\n  @Data\n  public static class Person {\n\n    @Searchable(tags = {\n            @Tag(fieldKey = \"p1.firstName\", entityFieldKey = \"p1FirstName\"),\n            @Tag(fieldKey = \"p2.firstName\", entityFieldKey = \"p2FirstName\"),\n    })\n    private String firstName;\n\n    @Searchable(tags = {\n            @Tag(fieldKey = \"p1.lastName\", entityFieldKey = \"p1LastName\"),\n            @Tag(fieldKey = \"p2.lastName\", entityFieldKey = \"p2LastName\"),\n    })\n    private String lastName;\n  }\n}\n```\n\n```bash\ncurl - request GET \n - url 'https://www.myexampledomain.com/couples?\np1.firstName_iEq=Romeo\n\u0026p2.firstName_iEq=Giulietta'\n```\n\n---  \n## Spring Boot Project example with HTTP Endpoint\nPlease note: this library does not expose any endpoints and therefore no controllers.\nAn example project, exhaustive and complete, is available  [here](https://github.com/biagioT/jpa-search-helper-demo).\n\n#### Mode 1\n\nController:\n```java\n@RestController\n@RequestMapping(\"/persons\")\npublic class PersonController {\n\n  @Autowired\n  private PersonManager personManager;\n\n  @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)\n  public List\u003cPerson\u003e findPersons(@RequestParam Map\u003cString, String\u003e requestParams) {\n    return personManager.find(requestParams);\n  }\n  \n  @GetMapping(path=\"/projection\", produces = MediaType.APPLICATION_JSON_VALUE)\n  public List\u003cPerson\u003e projection(@RequestParam Map\u003cString, String\u003e requestParams) {\n    return personManager.projection(requestParams);\n  }\n}\n```\n\nService/Manager bean:\n```java\n@Service\npublic class PersonManager {\n\n  @Autowired\n  private PersonRepository personRepository;\n\n  public List\u003cPerson\u003e find(Map\u003cString, String\u003e filters) {\n    return personRepository.findAllWithPaginationAndSorting(filters, Person.class).stream().map(this::toModel).toList();\n  }\n  \n  public List\u003cPerson\u003e projection(Map\u003cString, String\u003e filters) {\n    return personRepository.projection(filters, Person.class, PersonEntity.class).stream().map(this::toModel).toList();\n  }\n\n  private static Person toModel(PersonEntity personEntity) {\n    // ...\n  }\n  \n  private static Person toModel(Map\u003cString, Object\u003e entityMap) {\n    // ...\n  }\n\n}\n```\nCurl:\n```bash\ncurl - X GET \n'http://localhost:8080/persons?\nfirstName=Biagio\n\u0026lastName_startsWith=Toz\n\u0026birthDate_gte=19910101\n\u0026country_in=IT,FR,DE\n\u0026company.name_in=Microsoft,Apple\n\u0026company.employees_between=500,5000'\n```\n\nor\n\n```bash\ncurl - X GET \n'http://localhost:8080/persons/projection?\nfirstName=Biagio\n\u0026lastName_startsWith=Toz\n\u0026birthDate_gte=19910101\n\u0026country_in=IT,FR,DE\n\u0026company.name_in=Microsoft,Apple\n\u0026company.employees_between=500,5000\n\u0026selections=firstName,birthDate'\n```\n\n#### Mode 2\n\nController:\n```java\n@RestController\n@RequestMapping(\"/persons\")\n@Validated\npublic class PersonController {\n\n  @Autowired\n  private PersonManager personManager;\n\n  @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)\n  public List\u003cPerson\u003e findPersons(@Valid @RequestBody JPASearchInput input) {\n    return personManager.find(input);\n  }\n\n  @PostMapping(path=\"/projection\", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)\n  public List\u003cPerson\u003e projection(@Valid @RequestBody JPASearchInput input) {\n    return personManager.projection(input);\n  }\n}\n```\n\nService/Manager bean:\n```java\n@Service\npublic class PersonManager {\n\n  @Autowired\n  private PersonRepository personRepository;\n\n  public List\u003cPerson\u003e find(JPASearchInput input) {\n    return personRepository.findAllWithPaginationAndSorting(input, Person.class).stream().map(this::toModel).toList();\n  }\n  \n  public List\u003cPerson\u003e projection(JPASearchInput input) {\n    return personRepository.projection(input, Person.class, PersonEntity.class).stream().map(this::toModel).toList();\n  }\n\n  private static Person toModel(PersonEntity entity) {\n    // ...\n  }\n  \n  private static Person toModel(Map\u003cString, Object\u003e entityMap) {\n    // ...\n  }\n\n}\n```\nCurl:\n```bash\ncurl -X POST -H \"Content-type: application/json\" -d '{\n  \"filter\" : {\n      \"operator\": \"and\", // the first filter must contain a root operator: AND, OR or NOT\n      \"filters\" : [\n        {\n          \"operator\": \"eq\",\n          \"key\": \"firstName\",\n          \"value\": \"Biagio\"\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"startsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"Toz\",\n              \"options\": {\n                \"ignoreCase\": true\n              }\n            },\n            {\n              \"operator\": \"endsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"ZZI\",\n              \"options\": {\n                \"ignoreCase\": true,\n                \"trim\" : true\n              }\n            }\n          ]\n        },\n        {\n          \"operator\": \"in\",\n          \"key\": \"company.name\",\n          \"values\": [\"Microsoft\", \"Apple\", \"Google\"]\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"gte\",\n              \"key\": \"birthDate\",\n              \"value\": \"19910101\"\n            },\n            {\n              \"operator\": \"lte\",\n              \"key\": \"birthDate\",\n              \"value\": \"20010101\"\n            }\n          ]\n        },\n        {\n          \"operator\": \"between\",\n          \"key\" : \"company.employees\",\n          \"values\": [500, 5000],\n          \"options\": {\n            \"negate\": true\n          }\n        }\n      ]\n  },\n  \"options\": {\n    \"pageSize\": 10,\n    \"pageOffset\": 0,\n    \"sortOptions\" : [\n      {\n        \"key\": \"birthDate\",\n        \"desc\": false\n      }\n    ]\n  }\n  \n}' 'http://localhost:8080/persons'\n```\nor\n```bash\ncurl -X POST -H \"Content-type: application/json\" -d '{\n  \"filter\" : {\n      \"operator\": \"and\", // the first filter must contain a root operator: AND, OR or NOT\n      \"filters\" : [\n        {\n          \"operator\": \"eq\",\n          \"key\": \"firstName\",\n          \"value\": \"Biagio\"\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"startsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"Toz\",\n              \"options\": {\n                \"ignoreCase\": true\n              }\n            },\n            {\n              \"operator\": \"endsWith\",\n              \"key\": \"lastName\",\n              \"value\": \"ZZI\",\n              \"options\": {\n                \"ignoreCase\": true,\n                \"trim\" : true\n              }\n            }\n          ]\n        },\n        {\n          \"operator\": \"in\",\n          \"key\": \"company.name\",\n          \"values\": [\"Microsoft\", \"Apple\", \"Google\"]\n        },\n        {\n          \"operator\": \"or\",\n          \"filters\": [\n            {\n              \"operator\": \"gte\",\n              \"key\": \"birthDate\",\n              \"value\": \"19910101\"\n            },\n            {\n              \"operator\": \"lte\",\n              \"key\": \"birthDate\",\n              \"value\": \"20010101\"\n            }\n          ]\n        },\n        {\n          \"operator\": \"between\",\n          \"key\" : \"company.employees\",\n          \"values\": [500, 5000],\n          \"options\": {\n            \"negate\": true\n          }\n        }\n      ]\n  },\n  \"options\": {\n    \"sortOptions\" : [\n      {\n        \"key\": \"birthDate\",\n        \"desc\": false\n      }\n    ],\n    \"selections\" : [\n\t\t\"birthDate\",\n\t\t\"firstName\",\n\t\t\"lastName\"\n\t]\n  }\n  \n}' 'http://localhost:8080/persons/projection'\n```\n---  \n## Links\n- Presentation article (version 0.0.1) on [Medium](https://medium.com/@biagio.tozzi/advanced-and-dynamic-searching-with-spring-data-jpa-e322e0f40b4b)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbiagiot%2Fjpa-search-helper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbiagiot%2Fjpa-search-helper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbiagiot%2Fjpa-search-helper/lists"}