Skip to content

Commit

Permalink
Support filtering by named queries in REST Data with Panache extension
Browse files Browse the repository at this point in the history
After quarkusio#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".
  • Loading branch information
Sgitario committed Nov 17, 2022
1 parent ff044ff commit ce37f5c
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 11 deletions.
20 changes: 20 additions & 0 deletions docs/src/main/asciidoc/rest-data-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Long> {

}
Original file line number Diff line number Diff line change
@@ -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<Long> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Long> {

}
Original file line number Diff line number Diff line change
@@ -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<Long> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()),
Expand All @@ -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<String, ResultHandle> 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));
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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)));
Expand All @@ -257,6 +262,7 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou
Collection<SignatureMethodCreator.Parameter> compatibleFieldsForQuery = getFieldsToQuery(resourceMetadata);
List<SignatureMethodCreator.Parameter> 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()),
Expand All @@ -271,26 +277,28 @@ 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<String, ResultHandle> 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));
index++;
}

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)));
}
Expand All @@ -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<String, ResultHandle> fieldValues) {
ResultHandle page, ResultHandle sort, ResultHandle namedQuery, Map<String, ResultHandle> fieldValues) {

ResultHandle dataParams = creator.newInstance(ofConstructor(HashMap.class));
ResultHandle queryList = creator.newInstance(ofConstructor(ArrayList.class));
for (Map.Entry<String, ResultHandle> field : fieldValues.entrySet()) {
Expand All @@ -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) {
Expand Down

0 comments on commit ce37f5c

Please sign in to comment.