Spring Data JPA Pagination HHH000104 - spring-boot

I got this repository code:
#Query(value = "select distinct r from Reference r " +
"inner join fetch r.persons " +
"left outer join fetch r.categories " +
"left outer join fetch r.keywords " +
"left outer join fetch r.parentReferences",
countQuery = "select count(distinct r.id) from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences")
Page<Reference> findsAllRelevantEntries(Pageable pageable);
When i run tests on that query i got this Hibernate warning:
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
#Test
void testFindAllRelevantAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Page<Reference>> all = referenceService.findAllRelevantAsync(PageRequest.of(1, 20));
CompletableFuture.allOf(all).join();
assertThat(all.get()).isNotNull();
assertThat(all.get()).isNotEmpty();
}
The repository code is encapsulated in a service method not shown here. It (the service method) just marshalls the call from the service to the repository and back.
Furthermore the generated sql query does not generate a limit clause. Though it does fire two queries.
One for the count, the other for the fetching of all records.
So it fetches all records and applies the pagination in memory.
This leads to a very slow query execution.
How can i make pagination work with this query?
EDIT
I know that this here is often suggested as solution:
How can I avoid the Warning "firstResult/maxResults specified with collection fetch; applying in memory!" when using Hibernate?
Is there a way to achieve the pagination with Spring Data JPA?
I don´t want to hardwire either an EntityManager, neither i want to
extend code from a BasicTransformerAdapter

You could use a generic / reusable approach based on the two-queries approach.
One SQL query to retrieve the entities' IDs and a second query with an IN predicate including the IDs from the second query.
Implementing a custom Spring Data JPA Executor:
#NoRepositoryBean
public interface AsimioJpaSpecificationExecutor<E, ID extends Serializable> extends JpaSpecificationExecutor<E> {
Page<ID> findEntityIds(Pageable pageable);
}
public class AsimioSimpleJpaRepository<E, ID extends Serializable> extends SimpleJpaRepository<E, ID>
implements AsimioJpaSpecificationExecutor<E, ID> {
private final EntityManager entityManager;
private final JpaEntityInformation<E, ID> entityInformation;
public AsimioSimpleJpaRepository(JpaEntityInformation<E, ID> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
this.entityInformation = entityInformation;
}
#Override
public Page<ID> findEntityIds(Pageable pageable) {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
CriteriaQuery<ID> criteriaQuery = criteriaBuilder.createQuery(this.entityInformation.getIdType());
Root<E> root = criteriaQuery.from(this.getDomainClass());
// Get the entities ID only
criteriaQuery.select((Path<ID>) root.get(this.entityInformation.getIdAttribute()));
// Update Sorting
Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
if (sort.isSorted()) {
criteriaQuery.orderBy(toOrders(sort, root, criteriaBuilder));
}
TypedQuery<ID> typedQuery = this.entityManager.createQuery(criteriaQuery);
// Update Pagination attributes
if (pageable.isPaged()) {
typedQuery.setFirstResult((int) pageable.getOffset());
typedQuery.setMaxResults(pageable.getPageSize());
}
return PageableExecutionUtils.getPage(typedQuery.getResultList(), pageable,
() -> executeCountQuery(this.getCountQuery(null, this.getDomainClass())));
}
protected static long executeCountQuery(TypedQuery<Long> query) {
Assert.notNull(query, "TypedQuery must not be null!");
List<Long> totals = query.getResultList();
long total = 0L;
for (Long element : totals) {
total += element == null ? 0 : element;
}
return total;
}
}
You can read more at https://tech.asimio.net/2021/05/19/Fixing-Hibernate-HHH000104-firstResult-maxResults-warning-using-Spring-Data-JPA.html

I found a workaround myself. Based upon this:
How can I avoid the Warning "firstResult/maxResults specified with collection fetch; applying in memory!" when using Hibernate?
First: Get the Ids by pagination:
#Query(value = "select distinct r.id from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences " +
"order by r.id",
countQuery = "select count(distinct r.id) from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences " +
"order by r.id")
Page<UUID> findsAllRelevantEntriesIds(Pageable pageable);
Second: Use the Ids to do an in query
#Query(value = "select distinct r from Reference r " +
"inner join fetch r.persons " +
"left outer join fetch r.categories " +
"left outer join fetch r.keywords " +
"left outer join fetch r.parentReferences " +
"where r.id in ?1 " +
"order by r.id",
countQuery = "select count(distinct r.id) from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences ")
#QueryHints(value = {#QueryHint(name = "hibernate.query.passDistinctThrough", value = "false")},
forCounting = false)
List<Reference> findsAllRelevantEntriesByIds(UUID[] ids);
Note:
I get a List<Reference not a Pageable so you have to build your Pageable on your own like so:
private Page<Reference> processResults(Pageable pageable, Page<UUID> result) {
List<Reference> references = referenceRepository.findsAllRelevantEntriesByIds(result.toList().toArray(new UUID[0]));
return new PageImpl<>(references, pageable, references.size());
}
This looks not nice and does two statements, but it queries with limit, so only the needed records get fetched.

The approach to fetch ids first and then do the main query works but is not very efficient. I think this is a perfect use case for Blaze-Persistence.
Blaze-Persistence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. The pagination support it comes with handles all of the issues you might encounter.
It also has a Spring Data integration, so you can use the same code like you do now, you only have to add the dependency and do the setup: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-setup
Blaze-Persistence has many different strategies for pagination which you can configure. The default strategy is to inline the query for ids into the main query. Something like this:
select r
from Reference r
inner join r.persons
left join fetch r.categories
left join fetch r.keywords
left join fetch r.parentReferences
where r.id IN (
select r2.id
from Reference r2
inner join r2.persons
order by ...
limit ...
)
order by ...

Related

Fetch specific column by using multiple tables, using native query and interface based projection

my need is to use native query and interface-based projection in JPA to fetch the record from multiple tables
#Query(value = "select cm.center_code as centerCode,cm.center_name as centerName,es.exam_slot_code as slotName,ecm.total_seat_allocated as seatAllocatedCount,ecityM.city_name as centerCity,afcatg.name as groupName from center_master cm "
+ "inner join centre_examslot_mapping cesm "
+ "on cm.center_id = cesm.centre_id "
+ "inner join exam_city_master ecityM "
+ "on ecityM.city_id= cm.center_city_id "
+ "inner join exam_slot es "
+ "on cesm.exam_slot_id= es.exam_slot_id "
+ "inner join exam_center_mapping ecm "
+ "on cesm.centre_examslot_mappingid = ecm.centre_examslot_mappingid "
+ "inner join afcat_group afcatg "
+ "on ecm.afcat_group_id= afcatg.afcat_group_id "
+ "where cm.center_code= ?1", nativeQuery = true)
public List<GetSlotGroupSeatAllocatedFromDB> getData(String centerCode);
To use projections you have to define an interface with attributes you need with prefix 'get'. Your class GetSlotGroupSeatAllocatedFromDB must be an interface implemented like this:
public Interface GetSlotGroupSeatAllocatedFromDB{
Integer getCenterCode();
String getCenterName();
String getSlotName();
Integer getSeatAllocatedCount();
String getCenterCity();
String getGroupName();
}
I supposed the attributes types (Integer o String, maybe your centerCode is String...)

Spring JPA - Issue while sorting on non entity column

I have requirement where I need to get the records based in join of three table with pagination(addition requirement are also there). So I have written a nativeQuery to get the records. Below is the sample query
#Query(value = "SELECT "
+ "a.GROUP_REF_ID as refId "
+ "count(case when c.STAT_CD in :userStatus then (c.grp_user_id) end) as numberOfUsers, "
+ "count(case when b.STAT_CD in :itemStatus then (b.grp_item_id) end) as numberOfItems "
+ "from grp a left join grp_item b on a.grp_id=b.grp_id left join grp_user c on a.grp_id=c.grp_id "
+ "where a.stat_cd in :status and a.co_id in :cids "
+ "group by a.GROUP_REF_ID,a.grp_nam,a.GRP_DESC,a.co_id,a.co_nam,a.CRTE_BY, "
+ "a.CRTE_DT,a.UPDT_BY,a.UPDT_DT ", countQuery = "select count(*) from grp where stat_cd in :status and co_id in :cids ", nativeQuery = true)
public Page<Object> findByStatusAndCompanyIdIn(#Param("status") String status, #Param("cids") List<Long> companyIds,
#Param("userStatus") List<GroupUserStatus> userStatus,
#Param("itemStatus") List<GroupItemStatus> itemStatus, Pageable pageable);
Now the requirement is also that these records are to be sorted on any of the column in select part. So, if user passes numberOfItems, the records are to be sorted on it. But I am facing an issue here because if I pass Sort parameter as numberOfItems, spring prepends an a. before numberOfItems which results in error that not able to find a.numberOfItems.
Is there a way I can stop spring from prepending table alias while creating a query with Sort, or should I write my logic in a different approach
Making my comment an answer so the question can be marked as answered:
Wrap the whole select in another one: select * from (<your current select>) x
I have solved the issue by creating a projection. (Kotlin was used but you’ll get the gist.)
class ShowRepository : JpaRepository<Int, Show> {
#Query("SELECT s AS show, (CASE WHEN (s.status = 'scheduled') THEN s.scheduledStartTime ELSE s.startTime END) AS time FROM Show s")
fun findShows(pageable: Pageable): Page<ShowWithTime>
}
interface ShowWithTime {
val show: Show,
val time: Date?
}
This allows Spring-Data to work its full magic, and using Sort.by(Order.desc("time")) works like a charm.
I’ve written it up with a little bit more detail here: Sorting by a Non-Entity Field.

Spring Data - Custom DTO Query with filtering

I have a complexe application and I need to retrieve and filter 1000~5000 object for an xls export. Each object having multiple eager relationship (I need them for the export).
If I retrieve all the objects and their relationship as it is, I got some stackoverflow error.
Generaly when I need to make a big export, in order to make it efficient I use a DTO object with an #Query like this :
public interface myRepository extends JpaRepository<Car, Long> {
#Query("SELECT new com.blabla.myCustomObject(p.name, p.surname, c.model, c.number ...) "
+ "FROM Car c "
+ "LEFT JOIN c.person p "
+ "WHERE ... ")
List<myCustomObject> getExportCustomObject();
}
The problem is that the #Query is static and I want to add dynamic filter to my Query (Specifications, Criteria or some other system...)
How to do it ?
Specification cannot be used because this is only the where clause.
But you can use Criteria API. Here's an example. The BasicTeacherInfo is the DTO:
CriteriaQuery<BasicTeacherInfo> query = cb.createQuery(BasicTeacherInfo.class);
Root<Teacher> teacher = query.from(Teacher.class);
query.multiselect(teacher.get("firstName"),teacher.get("lastName"));
List<BasicTeacherInfo> results = em.createQuery(query).getResultList();
You can use #Param annotation to pass dynamic values to HQL, something like:
#Query("SELECT new com.blabla.myCustomObject(p.name, p.surname, c.model, c.number ...) "
+ "FROM Car c "
+ "LEFT JOIN c.person p "
+ "WHERE c.status = :status AND p.name = :name")
List<myCustomObject> getExportCustomObject(
#Param("status") Integer status,
#Param("name") String name
);
Below is one of the possible way where you can try to add offset and limit into your query you can make it dynamic with the help off placeholders.
Below is an sample pseudo code for reference:
Dao Layer:
#Query(value="SELECT e FROM tablename e WHERE condition_here ORDER BY e.id offset :offset limit:limit ")
public returnType yourMethod(String name, int offset, int limit);
Service Layer:
long count = number of records in db.
int a = // number of records to be fetched on each iterations
int num_iterations = count % a ;
int additionalrecords = count / a;
int start= 0;
while(num_iterations>0)
{
dao.yourMethod(start,a);
start = start+a;
count--;
// write your data to excel here
}
dao.yourMethod(start,additionalrecords);
Hope it is helpful.

Spring boot jpa select using multiple key values from joined map

I have the following working query which selects based on the key and value of a joined map.
#Query("select e from Entity e join e.dataAttributes da where " +
"(key(da) =:attrKey1 and :attrVal1 in (value(da)) )")
List<Entity> findByAttrributeValues(#Param("attrKey1") String attrKey1,
#Param("attrVal1") String attrVal1);
I would like to select based on 2 keys and 2 values, but am having difficulties. The following likely very naiieve attempt returns no results:
#Query("select e from Entity e join e.dataAttributes da where " +
"(key(da) =:attrKey1 and :attrVal1 in (value(da)) ) and " +
"(key(da) =:attrKey2 and :attrVal2 in (value(da)) )")
List<Entity> findByTwoAttrributeValues(#Param("attrKey1") String attrKey1,
#Param("attrVal1") String attrVal1,
#Param("attrKey2") String attrKey2,
#Param("attrVal2") String attrVal2);
I'm new to JPA, any guidence would be appreciated
So I have this solution :
#Query("select e from Entity e join e.dataAttributes da join e.dataAttributes da2 where " +
"(key(da) =:attrKey1 and :attrVal1 in (value(da)) ) and " +
"(key(da2) =:attrKey2 and :attrVal2 in (value(da2)) )")
List<Entity> findByTwoAttrributeValues(#Param("attrKey1") String attrKey1,
#Param("attrVal1") String attrVal1,
#Param("attrKey2") String attrKey2,
#Param("attrVal2") String attrVal2);
I needed to join again onto the dataAttributes with a second identifier. Works as expected now. Might not be the prettiest. If there's a better way let me know

Spring Data JPA Repository returns Object[] instead of MyType while using Sum Function

Until today I was using this statement:
#Query(value = "select top 5 p.*, sum(po.quantity) as total_quantity from product p " +
"inner join productorder po " +
"on p.id = po.product_id " +
"group by p.id, p.name " +
"order by total_quantity desc", nativeQuery = true)
List<Product> findTopFiveBestSellerNative();
Here as i define the return type as a list of Products, i exactly get what i need. And the selected column total_quantity is simply ignored.
Lastly i needed to integrate pagination into this query. Since Spring does not support pagination handling with native queries, i wanted to first transform this query to JPQL (then i will add pagination). And now it looks like this:
#Query(value = "select p, sum(po.quantity) as total_quantity " +
"from Product p, ProductOrder po " +
"where p.id = po.pk.product " +
"group by p.id, p.name " +
"order by total_quantity desc")
List<Product> findTopFiveBestSeller();
The return type is now a list of object arrays, where the first element of array is Product, and second one is the total_quantity. (Although the method signature says List..)
How can i change my statement or somehow achieve this, so that i do not have to deal with array, and simply just get my Product objects?
EDIT: I had the idea to use this query as a subquery, and just select the products from the subquery. It turns out that the JPQL cannot do subqueries in the 'from' clause..
Fact is your query is not returning only the columns needed to build a Product object, so Spring JPA is mapping it to an object.
Create an aggregate entity that extends product and that contains the property totalQuantity and map your query to it (possibly in another repository)

Resources