Skip to content

Commit

Permalink
ORM/Panache: rely on ORM getResultCount
Browse files Browse the repository at this point in the history
This allows us to not care about which parameters are set on the count
query, since `order by` parameters are ignored by ORM. Which is not
the case if we write our own count query.

Note that this is not yet available in HR, so we have to keep the count
query logic around until that's available.

Fixes #40962
  • Loading branch information
FroMage committed Jul 2, 2024
1 parent ca73df4 commit 941cc29
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ public void close() {
* this is the original Panache-Query, if any (can be null)
*/
private String originalQuery;
protected String countQuery;
/**
* This is only used by the Spring Data JPA extension, due to Spring's Query annotation allowing a custom count query
* See https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.at-query.native
* Otherwise we do not use this, and rely on ORM to generate count queries
*/
protected String customCountQueryForSpring;
private String orderBy;
private Session session;

Expand All @@ -73,11 +78,12 @@ public CommonPanacheQueryImpl(Session session, String query, String originalQuer
this.paramsArrayOrMap = paramsArrayOrMap;
}

private CommonPanacheQueryImpl(CommonPanacheQueryImpl<?> previousQuery, String newQueryString, String countQuery,
private CommonPanacheQueryImpl(CommonPanacheQueryImpl<?> previousQuery, String newQueryString,
String customCountQueryForSpring,
Class<?> projectionType) {
this.session = previousQuery.session;
this.query = newQueryString;
this.countQuery = countQuery;
this.customCountQueryForSpring = customCountQueryForSpring;
this.orderBy = previousQuery.orderBy;
this.paramsArrayOrMap = previousQuery.paramsArrayOrMap;
this.page = previousQuery.page;
Expand Down Expand Up @@ -106,16 +112,16 @@ public <T> CommonPanacheQueryImpl<T> project(Class<T> type) {

// If the query starts with a select clause, we pass it on to ORM which can handle that via a projection type
if (lowerCasedTrimmedQuery.startsWith("select ")) {
// just pass it through
return new CommonPanacheQueryImpl<>(this, query, countQuery, type);
// I think projections do not change the result count, so we can keep the custom count query
return new CommonPanacheQueryImpl<>(this, query, customCountQueryForSpring, type);
}

// FIXME: this assumes the query starts with "FROM " probably?

// build select clause with a constructor expression
String selectClause = "SELECT " + getParametersFromClass(type, null);
return new CommonPanacheQueryImpl<>(this, selectClause + selectQuery,
"select count(*) " + selectQuery, null);
// I think projections do not change the result count, so we can keep the custom count query
return new CommonPanacheQueryImpl<>(this, selectClause + selectQuery, customCountQueryForSpring, null);
}

private StringBuilder getParametersFromClass(Class<?> type, String parentParameter) {
Expand Down Expand Up @@ -267,35 +273,28 @@ public void withHint(String hintName, Object value) {

// Results

@SuppressWarnings("unchecked")
public long count() {
if (count == null) {
String selectQuery = query;
if (PanacheJpaUtil.isNamedQuery(query)) {
SelectionQuery q = session.createNamedSelectionQuery(query.substring(1));
selectQuery = getQueryString(q);
}

SelectionQuery countQuery = session.createSelectionQuery(countQuery(selectQuery));
if (paramsArrayOrMap instanceof Map)
AbstractJpaOperations.bindParameters(countQuery, (Map<String, Object>) paramsArrayOrMap);
else
AbstractJpaOperations.bindParameters(countQuery, (Object[]) paramsArrayOrMap);
try (NonThrowingCloseable c = applyFilters()) {
count = (Long) countQuery.getSingleResult();
if (customCountQueryForSpring != null) {
SelectionQuery<Long> countQuery = session.createSelectionQuery(customCountQueryForSpring, Long.class);
if (paramsArrayOrMap instanceof Map)
AbstractJpaOperations.bindParameters(countQuery, (Map<String, Object>) paramsArrayOrMap);
else
AbstractJpaOperations.bindParameters(countQuery, (Object[]) paramsArrayOrMap);
try (NonThrowingCloseable c = applyFilters()) {
count = countQuery.getSingleResult();
}
} else {
@SuppressWarnings("rawtypes")
org.hibernate.query.Query query = (org.hibernate.query.Query) createBaseQuery();
try (NonThrowingCloseable c = applyFilters()) {
count = query.getResultCount();
}
}
}
return count;
}

private String countQuery(String selectQuery) {
if (countQuery != null) {
return countQuery;
}

return PanacheJpaUtil.getFastCountQuery(selectQuery);
}

@SuppressWarnings("unchecked")
public <T extends Entity> List<T> list() {
SelectionQuery hibernateQuery = createQuery();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public CustomCountPanacheQuery(Session session, SelectionQuery hibernateQuery, S
super(new CommonPanacheQueryImpl<>(session, CommonPanacheQueryImpl.getQueryString(hibernateQuery),
null, null, paramsArrayOrMap) {
{
this.countQuery = customCountQuery;
this.customCountQueryForSpring = customCountQuery;
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class PanacheJpaUtil {
/**
* This turns an HQL (already expanded from Panache-QL) query into a count query, using text manipulation
* if we can, because it's faster, or fall back to using the ORM HQL parser in {@link #getCountQueryUsingParser(String)}
*
* @deprecated we should use SelectionQuery.getResultCount() when supported by HR (ORM already supports it)
*/
public static String getFastCountQuery(String query) {
// try to generate a good count query from the existing query
Expand Down Expand Up @@ -91,6 +93,8 @@ public static String getFastCountQuery(String query) {
/**
* This turns an HQL (already expanded from Panache-QL) query into a count query, using the
* ORM HQL parser. Slow version, see {@link #getFastCountQuery(String)} for the fast version.
*
* @deprecated we should use SelectionQuery.getResultCount() when supported by HR (ORM already supports it)
*/
public static String getCountQueryUsingParser(String query) {
HqlLexer lexer = new HqlLexer(CharStreams.fromString(query));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.quarkus.it.panache;

import jakarta.persistence.Entity;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Bug40962Entity extends PanacheEntity {
public String name;
public String location;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -1860,4 +1861,16 @@ public String testBug31117() {
Assertions.assertEquals(1, Person.delete("\r\n \n\ndelete\nfrom\n Person2\nwhere\nname = ?1", "foo"));
return "OK";
}

@GET
@Path("40962")
@Transactional
public String testBug40962() {
// should not throw
Bug40962Entity.find("name = :name ORDER BY locate(location, :location) DESC",
Map.of("name", "Demo", "location", "something")).count();
Bug40962Entity.find("FROM Bug40962Entity WHERE name = :name ORDER BY locate(location, :location) DESC",
Map.of("name", "Demo", "location", "something")).count();
return "OK";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,9 @@ public void testBug36496() {
public void testBug31117() {
RestAssured.when().get("/test/31117").then().body(is("OK"));
}

@Test
public void testBug40962() {
RestAssured.when().get("/test/40962").then().body(is("OK"));
}
}

0 comments on commit 941cc29

Please sign in to comment.