From ce37f5c9466990bb7d63bfba637922e2f13705bd Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 17 Nov 2022 08:26:31 +0100 Subject: [PATCH] Support filtering by named queries in REST Data with Panache extension After https://github.com/quarkusio/quarkus/pull/29212 (filter by entity fields) is supported, we can now use namedQueries when filtering. With these changes, you can specify a named query to filter when listing the entities. For example, having the following named query in your entity: ```java @Entity @NamedQuery(name = "Person.containsInName", query = "from Person where name like CONCAT('%', CONCAT(:name, '%'))") public class Person extends PanacheEntity { String name; } ``` In this example, we have added a named query to list all the persons that contains some text in the `name` field. Next, we can set a query param `namedQuery` when listing the entities using the generated resource with the name of the named query that we want to use, for example, calling `http://localhost:8080/people?namedQuery=#Person.containsInName&name=ter` would return all the persons which name contains the text "ter". --- docs/src/main/asciidoc/rest-data-panache.adoc | 20 ++++++++ .../deployment/AbstractGetMethodTest.java | 12 +++++ .../data/panache/deployment/entity/Item.java | 2 + .../panache/deployment/repository/Item.java | 2 + .../deployment/AbstractGetMethodTest.java | 12 +++++ .../data/panache/deployment/entity/Item.java | 2 + .../panache/deployment/repository/Item.java | 2 + .../methods/ListMethodImplementor.java | 47 ++++++++++++++----- 8 files changed, 88 insertions(+), 11 deletions(-) diff --git a/docs/src/main/asciidoc/rest-data-panache.adoc b/docs/src/main/asciidoc/rest-data-panache.adoc index c3e931ec0de92..7846ecc2c62fd 100644 --- a/docs/src/main/asciidoc/rest-data-panache.adoc +++ b/docs/src/main/asciidoc/rest-data-panache.adoc @@ -371,6 +371,7 @@ It applies to the paged resources only and is a number starting with 1. Default * `sort` - a comma separated list of fields which should be used for sorting a result of a list operation. Fields are sorted in the ascending order unless they're prefixed with a `-`. E.g. `?sort=name,-age` will sort the result by the name ascending by the age descending. +* `namedQuery` - a named query that should be configured at entity level using the annotation `@NamedQuery`. For example, if you want to get two `People` entities in the first page, you should call `http://localhost:8080/people?page=0&size=2`, and the response should look like: @@ -405,6 +406,25 @@ Additionally, you can also filter by the entity fields by adding a query param w IMPORTANT: Filtering by fields is only supported for primitive types. +== Complex filtering to list entities using @NamedQuery + +You can specify a named query to filter when listing the entities. For example, having the following named query in your entity: + +[source,java] +---- +@Entity +@NamedQuery(name = "Person.containsInName", query = "from Person where name like CONCAT('%', CONCAT(:name, '%'))") +public class Person extends PanacheEntity { + String name; +} +---- + +In this example, we have added a named query to list all the persons that contains some text in the `name` field. + +Next, we can set a query param `namedQuery` when listing the entities using the generated resource with the name of the named query that we want to use, for example, calling `http://localhost:8080/people?namedQuery=Person.containsInName&name=ter` would return all the persons which name contains the text "ter". + +For more information about how named queries work, go to https://quarkus.io/guides/hibernate-orm-panache#named-queries[the Hibernate ORM guide] or to https://quarkus.io/guides/hibernate-reactive-panache#named-queries[the Hibernate Reactive guide]. + == Resource Method Before/After Listeners REST Data with Panache supports the subscription to the following resource method hooks: diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractGetMethodTest.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractGetMethodTest.java index 769ca585a5edb..4f9dd7823d60c 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractGetMethodTest.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/AbstractGetMethodTest.java @@ -109,6 +109,18 @@ void shouldListWithManyFilters() { .and().body("name", contains("first")); } + @Test + void shouldListWithNamedQuery() { + given().accept("application/json") + .when() + .queryParam("name", "s") + .queryParam("namedQuery", "Item.containsInName") + .get("/items") + .then().statusCode(200) + .and().body("id", contains(1, 2)) + .and().body("name", contains("first", "second")); + } + @Test void shouldListSimpleHalObjects() { given().accept("application/hal+json") diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/Item.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/Item.java index a1f94de46c346..1b69b84afb9d2 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/Item.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/entity/Item.java @@ -1,8 +1,10 @@ package io.quarkus.hibernate.orm.rest.data.panache.deployment.entity; import javax.persistence.Entity; +import javax.persistence.NamedQuery; @Entity +@NamedQuery(name = "Item.containsInName", query = "from Item where name like CONCAT('%', CONCAT(:name, '%'))") public class Item extends AbstractItem { } diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/repository/Item.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/repository/Item.java index 86a46ea9cee76..da0e9cb0d59eb 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/repository/Item.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/repository/Item.java @@ -1,8 +1,10 @@ package io.quarkus.hibernate.orm.rest.data.panache.deployment.repository; import javax.persistence.Entity; +import javax.persistence.NamedQuery; @Entity +@NamedQuery(name = "Item.containsInName", query = "from Item where name like CONCAT('%', CONCAT(:name, '%'))") public class Item extends AbstractItem { } diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractGetMethodTest.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractGetMethodTest.java index ab2776bd0dcbb..ca6ebebabacd4 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractGetMethodTest.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/AbstractGetMethodTest.java @@ -109,6 +109,18 @@ void shouldListWithManyFilters() { .and().body("name", contains("first")); } + @Test + void shouldListWithNamedQuery() { + given().accept("application/json") + .when() + .queryParam("name", "s") + .queryParam("namedQuery", "Item.containsInName") + .get("/items") + .then().statusCode(200) + .and().body("id", contains(1, 2)) + .and().body("name", contains("first", "second")); + } + @Test void shouldListSimpleHalObjects() { given().accept("application/hal+json") diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/Item.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/Item.java index e5dc5a8edf83a..1dafc18413e99 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/Item.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/entity/Item.java @@ -1,8 +1,10 @@ package io.quarkus.hibernate.reactive.rest.data.panache.deployment.entity; import javax.persistence.Entity; +import javax.persistence.NamedQuery; @Entity +@NamedQuery(name = "Item.containsInName", query = "from Item where name like CONCAT('%', CONCAT(:name, '%'))") public class Item extends AbstractItem { } diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/repository/Item.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/repository/Item.java index 803a5ab96e13c..c380ccc0406e1 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/repository/Item.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/repository/Item.java @@ -1,8 +1,10 @@ package io.quarkus.hibernate.reactive.rest.data.panache.deployment.repository; import javax.persistence.Entity; +import javax.persistence.NamedQuery; @Entity +@NamedQuery(name = "Item.containsInName", query = "from Item where name like CONCAT('%', CONCAT(:name, '%'))") public class Item extends AbstractItem { } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java index 784d0c55b3108..42d513e83eef5 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java @@ -28,6 +28,8 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.gizmo.AnnotatedElement; +import io.quarkus.gizmo.AssignableResultHandle; +import io.quarkus.gizmo.BranchResult; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.FieldDescriptor; @@ -174,6 +176,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource parameters.add(param("page", int.class)); parameters.add(param("size", int.class)); parameters.add(param("uriInfo", UriInfo.class)); + parameters.add(param("namedQuery", String.class)); parameters.addAll(compatibleFieldsForQuery); MethodCreator methodCreator = SignatureMethodCreator.getMethodCreator(getMethodName(), classCreator, isNotReactivePanache() ? ofType(Response.class) : ofType(Uni.class, resourceMetadata.getEntityType()), @@ -194,8 +197,9 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource addQueryParamAnnotation(methodCreator.getParameterAnnotations(2), "size"); addDefaultValueAnnotation(methodCreator.getParameterAnnotations(2), Integer.toString(DEFAULT_PAGE_SIZE)); addContextAnnotation(methodCreator.getParameterAnnotations(3)); + addQueryParamAnnotation(methodCreator.getParameterAnnotations(4), "namedQuery"); Map fieldValues = new HashMap<>(); - int index = 4; + int index = 5; for (SignatureMethodCreator.Parameter param : compatibleFieldsForQuery) { addQueryParamAnnotation(methodCreator.getParameterAnnotations(index), param.getName()); fieldValues.put(param.getName(), methodCreator.getMethodParam(index)); @@ -209,6 +213,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource ResultHandle pageSize = methodCreator.getMethodParam(2); ResultHandle page = paginationImplementor.getPage(methodCreator, pageIndex, pageSize); ResultHandle uriInfo = methodCreator.getMethodParam(3); + ResultHandle namedQuery = methodCreator.getMethodParam(4); if (isNotReactivePanache()) { TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); @@ -219,7 +224,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource resource, page); ResultHandle links = paginationImplementor.getLinks(tryBlock, uriInfo, page, pageCount); - ResultHandle entities = list(tryBlock, resourceMetadata, resource, page, sort, fieldValues); + ResultHandle entities = list(tryBlock, resourceMetadata, resource, page, sort, namedQuery, fieldValues); // Return response returnValueWithLinks(tryBlock, resourceMetadata, resourceProperties, entities, links); @@ -234,7 +239,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource (body, pageCount) -> { ResultHandle pageCountAsInt = body.checkCast(pageCount, Integer.class); ResultHandle links = paginationImplementor.getLinks(body, uriInfo, page, pageCountAsInt); - ResultHandle uniEntities = list(body, resourceMetadata, resource, page, sort, fieldValues); + ResultHandle uniEntities = list(body, resourceMetadata, resource, page, sort, namedQuery, fieldValues); body.returnValue(UniImplementor.map(body, uniEntities, EXCEPTION_MESSAGE, (listBody, list) -> returnValueWithLinks(listBody, resourceMetadata, resourceProperties, list, links))); @@ -257,6 +262,7 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou Collection compatibleFieldsForQuery = getFieldsToQuery(resourceMetadata); List parameters = new ArrayList<>(); parameters.add(param("sort", List.class)); + parameters.add(param("namedQuery", String.class)); parameters.addAll(compatibleFieldsForQuery); MethodCreator methodCreator = SignatureMethodCreator.getMethodCreator(getMethodName(), classCreator, isNotReactivePanache() ? ofType(Response.class) : ofType(Uni.class, resourceMetadata.getEntityType()), @@ -271,8 +277,9 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou addOpenApiResponseAnnotation(methodCreator, Response.Status.OK, resourceMetadata.getEntityType(), true); addSecurityAnnotations(methodCreator, resourceProperties); addQueryParamAnnotation(methodCreator.getParameterAnnotations(0), "sort"); + addQueryParamAnnotation(methodCreator.getParameterAnnotations(1), "namedQuery"); Map fieldValues = new HashMap<>(); - int index = 1; + int index = 2; for (SignatureMethodCreator.Parameter param : compatibleFieldsForQuery) { addQueryParamAnnotation(methodCreator.getParameterAnnotations(index), param.getName()); fieldValues.put(param.getName(), methodCreator.getMethodParam(index)); @@ -280,17 +287,18 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou } ResultHandle sortQuery = methodCreator.getMethodParam(0); + ResultHandle namedQuery = methodCreator.getMethodParam(1); ResultHandle sort = sortImplementor.getSort(methodCreator, sortQuery); ResultHandle resource = methodCreator.readInstanceField(resourceFieldDescriptor, methodCreator.getThis()); if (isNotReactivePanache()) { TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); - ResultHandle entities = list(tryBlock, resourceMetadata, resource, null, sort, fieldValues); + ResultHandle entities = list(tryBlock, resourceMetadata, resource, null, sort, namedQuery, fieldValues); returnValue(tryBlock, resourceMetadata, resourceProperties, entities); tryBlock.close(); } else { ResultHandle uniEntities = list(methodCreator, resourceMetadata, resource, methodCreator.loadNull(), sort, - fieldValues); + namedQuery, fieldValues); methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntities, EXCEPTION_MESSAGE, (body, entities) -> returnValue(body, resourceMetadata, resourceProperties, entities))); } @@ -299,7 +307,8 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou } public ResultHandle list(BytecodeCreator creator, ResourceMetadata resourceMetadata, ResultHandle resource, - ResultHandle page, ResultHandle sort, Map fieldValues) { + ResultHandle page, ResultHandle sort, ResultHandle namedQuery, Map fieldValues) { + ResultHandle dataParams = creator.newInstance(ofConstructor(HashMap.class)); ResultHandle queryList = creator.newInstance(ofConstructor(ArrayList.class)); for (Map.Entry field : fieldValues.entrySet()) { @@ -313,13 +322,29 @@ public ResultHandle list(BytecodeCreator creator, ResourceMetadata resourceMetad dataParams, fieldValueFromQueryIsSet.load(fieldName), fieldValueFromQuery); } + /** + * String query; + * if (namedQuery != null) { + * query = "#" + namedQuery; + * } else { + * query = String.join(" AND ", queryList); + * } + */ + AssignableResultHandle query = creator.createVariable(String.class); + BranchResult checkIfNamedQueryIsNull = creator.ifNull(namedQuery); + BytecodeCreator whenNamedQueryIsNull = checkIfNamedQueryIsNull.trueBranch(); + BytecodeCreator whenNamedQueryIsNotNull = checkIfNamedQueryIsNull.falseBranch(); + whenNamedQueryIsNotNull.assign(query, whenNamedQueryIsNotNull.invokeVirtualMethod( + ofMethod(String.class, "concat", String.class, String.class), + whenNamedQueryIsNotNull.load("#"), namedQuery)); + whenNamedQueryIsNull.assign(query, whenNamedQueryIsNull.invokeStaticMethod( + ofMethod(String.class, "join", String.class, CharSequence.class, Iterable.class), + creator.load(" AND "), queryList)); + return creator.invokeVirtualMethod( ofMethod(resourceMetadata.getResourceClass(), "list", isNotReactivePanache() ? List.class : Uni.class, Page.class, Sort.class, String.class, Map.class), - resource, page == null ? creator.loadNull() : page, sort, - creator.invokeStaticMethod(ofMethod(String.class, "join", String.class, CharSequence.class, Iterable.class), - creator.load(" AND "), queryList), - dataParams); + resource, page == null ? creator.loadNull() : page, sort, query, dataParams); } private boolean isFieldTypeCompatibleForQueryParam(Type fieldType) {