Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.

Commit 773b307

Browse files
committed
SearchQueryBuilder and BrAPIRepositoryImpl overhaul for germ optimization
Added utility methods to SearchQueryBuilder and BrAPIRepositoryImpl to allow for proper paginating for hibernate fetch queries that don't suffocate memory. Also added methods to run queries without pagination entirely using the SearchQueryBuilder to prevent the use of pagination when it's not required, an issue that specifically had to be addressed for the BI cache, but one that introduced code that is reusable for other use cases. Modified the GermplasmApiController's searchGermplasmPost endpoint to accomodate two code paths: - One where no page and pageSize are supplied. In this scenario the code will grab all germplasm without the use of pagination. Good for large data grabs, but gets dangerous with excessively large amounts of data. This is entirely to meet BI's current use case, which we have strongly advised they move off of. - When page and/or pageSize are supplied, paginate as requested, default page size of 1000 if not requested.
1 parent ec4055a commit 773b307

36 files changed

+410
-151
lines changed

src/main/java/org/brapi/test/BrAPITestServer/controller/germ/GermplasmApiController.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,37 @@ public ResponseEntity<? extends BrAPIResponse> searchGermplasmPost(@RequestBody
213213
log.debug("Request: " + request.getRequestURI());
214214
validateSecurityContext(request, "ROLE_ANONYMOUS", "ROLE_USER");
215215
validateAcceptHeader(request);
216-
Metadata metadata = generateMetaDataTemplate(body);
217216

218217
String searchReqDbId = searchService.saveSearchRequest(body, SearchRequestTypes.GERMPLASM);
219218
if (searchReqDbId != null) {
220219
return responseAccepted(searchReqDbId);
220+
}
221+
222+
// WARN: This code was introduced to deal with a specific use case from BI which requires all data associated with
223+
// a particular program to be retreived at once. This method of data retreival is highly unadvised and can come
224+
// with serious performance deficits, such as slow response times and exhausted memory allocation.
225+
// Benchmarking suggests that at around 245-275k germplasm records returned 8GB of allocated memory will fail to
226+
// be enough to return a result.
227+
228+
// This code is a stop-gap to allow BI to continue to do this improper retreival in a way that will be efficient
229+
// for their specific use case.
230+
231+
// To get the data in this ill-advised way, forgo sending a page or pageSize attribute in the germplasm search request
232+
// to this endpoint. This will trigger the findGermplasmWithoutPaging code, which will grab all of the data without regard
233+
// to data size.
234+
235+
// To use the endpoint the right way, ensure one or both of the aforementioned attributes are set and the germplasm
236+
// records will be retrieved and returned paginated to limit resource consumption. This way is much more fine tuned
237+
// and will result in fast retrieval times with minimal memory allocation.
238+
if (body.getPage() == null && body.getPageSize() == null) {
239+
log.debug("Retrieving germs without pagination");
240+
List<Germplasm> data = germplasmService.findGermplasmWithoutPaging(body);
241+
Metadata metadata = generateEmptyMetadata();
242+
metadata.getPagination().setTotalCount(data.size());
243+
return responseOK(new GermplasmListResponse(), new GermplasmListResponseResult(), data, metadata);
221244
} else {
245+
log.debug("Retrieving germs with pagination");
246+
Metadata metadata = generateMetaDataTemplate(body);
222247
List<Germplasm> data = germplasmService.findGermplasm(body, metadata);
223248
return responseOK(new GermplasmListResponse(), new GermplasmListResponseResult(), data, metadata);
224249
}

src/main/java/org/brapi/test/BrAPITestServer/repository/BrAPIRepository.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
@NoRepositoryBean
1616
public interface BrAPIRepository<T extends BrAPIPrimaryEntity, ID extends Serializable> extends JpaRepository<T, ID> {
1717

18-
public Page<T> findAllBySearch(SearchQueryBuilder<T> searchQuery, Pageable pageReq);
18+
public Page<T> findAllBySearchAndPaginate(SearchQueryBuilder<T> searchQuery, Pageable pageReq);
1919

20-
public Page<UUID> findAllBySearchIdsOnly(SearchQueryBuilder<T> searchQuery, Pageable pageReq);
20+
public Page<T> findAllBySearchPaginatingWithFetches(SearchQueryBuilder<T> searchQuery, Pageable pageReq);
2121

22-
public Page<T> findAllBySearchNoPage(SearchQueryBuilder<T> searchQuery, Page<UUID> pagedIds, Pageable pageReq);
22+
public List<T> findAllBySearch(SearchQueryBuilder<T> searchQuery);
2323

2424
public Optional<T> findById(ID id);
2525

src/main/java/org/brapi/test/BrAPITestServer/repository/BrAPIRepositoryImpl.java

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
package org.brapi.test.BrAPITestServer.repository;
22

3+
import java.io.Serializable;
4+
import java.util.*;
5+
import java.util.Map.Entry;
6+
import java.util.stream.Collectors;
7+
8+
import io.swagger.model.Metadata;
9+
import io.swagger.model.germ.GermplasmSearchRequest;
10+
import javax.persistence.EntityManager;
11+
import javax.persistence.TypedQuery;
12+
313
import org.brapi.test.BrAPITestServer.model.entity.BrAPIBaseEntity;
414
import org.brapi.test.BrAPITestServer.model.entity.BrAPIPrimaryEntity;
515
import org.brapi.test.BrAPITestServer.model.entity.ExternalReferenceEntity;
@@ -15,53 +25,70 @@
1525
import org.springframework.security.core.context.SecurityContext;
1626
import org.springframework.security.core.context.SecurityContextHolder;
1727

18-
import javax.persistence.EntityManager;
19-
import javax.persistence.TypedQuery;
20-
import java.io.Serializable;
21-
import java.util.*;
22-
import java.util.Map.Entry;
23-
import java.util.stream.Collectors;
24-
2528
public class BrAPIRepositoryImpl<T extends BrAPIPrimaryEntity, ID extends Serializable>
2629
extends SimpleJpaRepository<T, ID> implements BrAPIRepository<T, ID> {
27-
private EntityManager entityManager;
30+
private final EntityManager entityManager;
2831

2932
public BrAPIRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
3033
super(entityInformation, entityManager);
3134
this.entityManager = entityManager;
3235
}
3336

34-
// This is the method that should be used for simple entities with few collections and eager loading requirements.
35-
public Page<T> findAllBySearch(SearchQueryBuilder<T> searchQuery, Pageable pageReq) {
37+
/**
38+
* Use this method to page simple entities with no lazily loaded collections or attributes.
39+
* WARN: Failure to do so can easily exhaust memory at scale and will cause hibernate warnings.
40+
*/
41+
public Page<T> findAllBySearchAndPaginate(SearchQueryBuilder<T> searchQuery, Pageable pageReq) {
3642
applyUserId(searchQuery);
3743
List<T> content = getPagedContent(searchQuery, pageReq);
3844
Long totalCount = getTotalCount(searchQuery);
3945

40-
Page<T> page = new PageImpl<>(content, pageReq, totalCount);
41-
42-
return page;
46+
return new PageImpl<>(content, pageReq, totalCount);
4347
}
4448

45-
public Page<T> findAllBySearchNoPage(SearchQueryBuilder<T> searchQuery, Page<UUID> pagedIds, Pageable pageReq) {
49+
/**
50+
* This method should be used when there is a need to page entities that are complex and have lots of lazily loaded,
51+
* one-to-many collection attributes. Call this method with the query you've built.
52+
* WARN: To avoid multiple bag fetch hibernate errors, only one collection that is one-many can be fetched at a time.
53+
* Ensure only one of these types of entity members is being fetched at time.
54+
* See {@link org.brapi.test.BrAPITestServer.service.germ.GermplasmService#findGermplasmEntities(GermplasmSearchRequest, Metadata)} for example usage.
55+
*
56+
* Once you fetch the first lazy loaded collection, if there are others you need to fetch for your entity it is
57+
* recommended to utilize the non-paging findAllBySearch() method, creating a searchQuery that inserts all the ids
58+
* of the entities found in the results from this method. Again, follow code path of above example usage to see how this is done.
59+
*
60+
* If the need is to fetch according to an entirely different base criteria, feel free to call this method again.
61+
*
62+
* The results will be paged and ordered based on the submitted searchQuery and pageReq.
63+
*/
64+
public Page<T> findAllBySearchPaginatingWithFetches(SearchQueryBuilder<T> searchQuery, Pageable pageReq) {
4665
applyUserId(searchQuery);
4766

48-
List<T> entities = searchEntitiesWithIds(searchQuery, pagedIds);
67+
// First grab all the ids of the entities according to the criteria of the searchQuery, paging as specified in the pageReq.
68+
List<UUID> content = getPagedContentIdsOnly(searchQuery, pageReq);
69+
Long totalCount = getTotalCount(searchQuery);
70+
71+
Page<UUID> pagedIds = new PageImpl<>(content, pageReq, totalCount);
4972

50-
return new PageImpl<>(entities, pageReq, pagedIds.getTotalElements());
73+
// Now execute another query to fetch all the entities requested in the searchQuery, passing the pagedIds found
74+
// in the previous query. We will fetch them utilizing the ids from the paged query, avoiding the hibernate
75+
// warning of paging while fetching and consuming considerably less memory.
76+
return findAllBySearchUsingIds(searchQuery, pagedIds, pageReq);
5177
}
5278

79+
private Page<T> findAllBySearchUsingIds(SearchQueryBuilder<T> searchQuery, Page<UUID> pagedIds, Pageable pageReq) {
80+
List<T> entities = searchEntitiesWithIds(searchQuery, pagedIds.toList());
5381

82+
return new PageImpl<>(entities, pageReq, pagedIds.getSize());
83+
}
5484

55-
// For entities that are complex and have lots of Collections and lazy loaded entities, it is more efficient to grab the IDs of the on Page,
56-
// and afterward fetch these collections using the Ids, this time without paging.
57-
public Page<UUID> findAllBySearchIdsOnly(SearchQueryBuilder<T> searchQuery, Pageable pageReq) {
85+
/**
86+
* Use this method to run the searchQuery without pagination. Useful for use cases where calls need to grab
87+
* every result at once, but use sparingly for use cases returning large result sets.
88+
*/
89+
public List<T> findAllBySearch(SearchQueryBuilder<T> searchQuery) {
5890
applyUserId(searchQuery);
59-
List<UUID> content = getPagedContentIdsOnly(searchQuery, pageReq);
60-
Long totalCount = getTotalCount(searchQuery);
61-
62-
Page<UUID> page = new PageImpl<>(content, pageReq, totalCount);
63-
64-
return page;
91+
return searchEntities(searchQuery);
6592
}
6693

6794
public Optional<T> findById(ID id) {
@@ -98,7 +125,7 @@ public void fetchXrefs(Page<T> page, Class<T> searchClass) {
98125
searchQuery.leftJoinFetch("externalReferences", "externalReferences")
99126
.appendList(page.stream().map(BrAPIBaseEntity::getId).collect(Collectors.toList()), "id");
100127

101-
Page<T> xrefs = findAllBySearch(searchQuery, PageRequest.of(0, page.getSize()));
128+
Page<T> xrefs = findAllBySearchAndPaginate(searchQuery, PageRequest.of(0, page.getSize()));
102129

103130
Map<String, List<ExternalReferenceEntity>> xrefByEntity = new HashMap<>();
104131
xrefs.forEach(entity -> xrefByEntity.put(entity.getId(), entity.getExternalReferences()));
@@ -127,28 +154,37 @@ private List<T> getPagedContent(SearchQueryBuilder<T> searchQuery, Pageable page
127154
TypedQuery<T> query = entityManager.createQuery(searchQuery.getQuery(), searchQuery.getClazz());
128155
query.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false);
129156

130-
for (Entry<String, Object> entry : searchQuery.getParams().entrySet()) {
131-
query.setParameter(entry.getKey(), entry.getValue());
132-
}
157+
setQueryParams(query, searchQuery);
133158

134159
query.setFirstResult((int) pageReq.getOffset());
135160
query.setMaxResults(pageReq.getPageSize());
136161

137-
List<T> content = query.getResultList();
138-
return content;
162+
return query.getResultList();
139163
}
140164

141-
private List<T> searchEntitiesWithIds(SearchQueryBuilder<T> searchQuery, Page<UUID> ids) {
142-
searchQuery.appendList(ids.stream().map(UUID::toString).collect(Collectors.toList()), "id");
165+
private List<T> searchEntitiesWithIds(SearchQueryBuilder<T> searchQuery, List<UUID> ids) {
166+
searchQuery.appendList(ids.stream().map(Object::toString).collect(Collectors.toList()), "id");
167+
168+
TypedQuery<T> query = entityManager.createQuery(searchQuery.getQuery(), searchQuery.getClazz());
169+
170+
setQueryParams(query, searchQuery);
143171

172+
return query.getResultList();
173+
}
174+
175+
private List<T> searchEntities(SearchQueryBuilder<T> searchQuery) {
144176
TypedQuery<T> query = entityManager.createQuery(searchQuery.getQuery(), searchQuery.getClazz());
145177

178+
setQueryParams(query, searchQuery);
179+
180+
return query.getResultList();
181+
}
182+
183+
private void setQueryParams(TypedQuery<T> query, SearchQueryBuilder<T> searchQuery) {
146184
for (Entry<String, Object> entry : searchQuery.getParams().entrySet()) {
147185
query.setParameter(entry.getKey(), entry.getValue());
148186
}
149-
150-
List<T> content = query.getResultList();
151-
return content; }
187+
}
152188

153189
private List<UUID> getPagedContentIdsOnly(SearchQueryBuilder<T> searchQuery, Pageable pageReq) {
154190

@@ -161,8 +197,7 @@ private List<UUID> getPagedContentIdsOnly(SearchQueryBuilder<T> searchQuery, Pag
161197
query.setFirstResult((int) pageReq.getOffset());
162198
query.setMaxResults(pageReq.getPageSize());
163199

164-
List<UUID> content = query.getResultList();
165-
return content;
200+
return query.getResultList();
166201
}
167202

168203

src/main/java/org/brapi/test/BrAPITestServer/service/SearchQueryBuilder.java

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package org.brapi.test.BrAPITestServer.service;
22

33
import java.math.BigDecimal;
4-
import java.util.ArrayList;
5-
import java.util.Date;
6-
import java.util.HashMap;
7-
import java.util.List;
8-
import java.util.Map;
4+
import java.util.*;
95

106
import java.time.LocalDate;
117
import java.time.OffsetDateTime;
@@ -32,7 +28,7 @@ public SearchQueryBuilder(Class<T> clazz) {
3228
}
3329

3430
public String getQuery() {
35-
if (sortClause.isBlank()) {
31+
if (sortClause.isEmpty()) {
3632
// By default, sort on entity id to have query result remain idempotent
3733
sortClause = " ORDER BY entity.id ASC ";
3834
}
@@ -41,7 +37,7 @@ public String getQuery() {
4137
}
4238

4339
public String getIdQuery() {
44-
if (sortClause.isBlank()) {
40+
if (sortClause.isEmpty()) {
4541
// By default, sort on entity id to have query result remain idempotent
4642
sortClause = " ORDER BY entity.id ASC ";
4743
}
@@ -66,6 +62,15 @@ public SearchQueryBuilder<T> appendList(List<String> list, String columnName) {
6662
return this;
6763
}
6864

65+
public SearchQueryBuilder<T> appendIds(List<String> ids) {
66+
String paramName = paramFilter("id");
67+
if (ids != null && !ids.isEmpty()) {
68+
this.whereClause += "AND " + entityPrefix("id") + " in :" + paramName + " ";
69+
this.params.put(paramName, ids);
70+
}
71+
return this;
72+
}
73+
6974
public SearchQueryBuilder<T> appendIntList(List<Integer> list, String columnName) {
7075
String paramName = paramFilter(columnName);
7176
if (list != null && !list.isEmpty()) {
@@ -242,10 +247,39 @@ public SearchQueryBuilder<T> join(String join, String name) {
242247
}
243248

244249
public SearchQueryBuilder<T> leftJoinFetch(String join, String name) {
245-
this.selectClause += "LEFT JOIN FETCH " + entityPrefix(join) + " " + paramFilter(name) + " ";
250+
this.selectClause += generateLeftJoinFetch(join, name);
251+
return this;
252+
}
253+
254+
/**
255+
* Use this method to remove left join fetches from specific collection attributes so you can leverage the same query to
256+
* iterate through other lazily loaded collections on an entity you need to fetch.
257+
*/
258+
public SearchQueryBuilder<T> removeAndReplaceLeftJoinFetch(String join,
259+
String name,
260+
String existingJoin,
261+
String existingName) {
262+
263+
this.selectClause =
264+
this.selectClause.replace(generateLeftJoinFetch(existingJoin, existingName), generateLeftJoinFetch(join, name));
265+
246266
return this;
247267
}
248268

269+
/**
270+
* Use this method to remove left join fetches from specific collection attributes so you can leverage the same
271+
* base query criteria to add another join fetch with leftJoinFetch()
272+
*/
273+
public SearchQueryBuilder<T> removeLeftJoinFetch(String join, String name) {
274+
this.selectClause =
275+
this.selectClause.replace(generateLeftJoinFetch(join, name), "");
276+
return this;
277+
}
278+
279+
private String generateLeftJoinFetch(String join, String paramName) {
280+
return "LEFT JOIN FETCH " + entityPrefix(join) + " " + paramFilter(paramName) + " ";
281+
}
282+
249283
private String entityPrefix(String field) {
250284
if (field.startsWith("*")) {
251285
return field.substring(1);

src/main/java/org/brapi/test/BrAPITestServer/service/core/ListService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public List<ListSummary> findLists(ListSearchRequest request, Metadata metadata)
8888
Pageable pageReq = PagingUtility.getPageRequest(metadata);
8989
SearchQueryBuilder<ListEntity> searchQuery = buildQueryString(request);
9090

91-
Page<ListEntity> entityPage = listRepository.findAllBySearch(searchQuery, pageReq);
91+
Page<ListEntity> entityPage = listRepository.findAllBySearchAndPaginate(searchQuery, pageReq);
9292

9393
List<ListSummary> data = entityPage.map(this::convertToSummary).getContent();
9494
PagingUtility.calculateMetaData(metadata, entityPage);

src/main/java/org/brapi/test/BrAPITestServer/service/core/LocationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public List<Location> findLocations(LocationSearchRequest request, Metadata meta
8383
.appendList(request.getParentLocationDbIds(), "parentLocation.id")
8484
.appendList(request.getParentLocationNames(), "parentLocation.name");
8585

86-
Page<LocationEntity> entityPage = locationRepository.findAllBySearch(searchQuery, pageReq);
86+
Page<LocationEntity> entityPage = locationRepository.findAllBySearchAndPaginate(searchQuery, pageReq);
8787

8888
List<Location> data = entityPage.map(this::convertFromEntity).getContent();
8989
PagingUtility.calculateMetaData(metadata, entityPage);

src/main/java/org/brapi/test/BrAPITestServer/service/core/PeopleService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public List<Person> findPeople(PersonSearchRequest request, Metadata metadata) {
6262
.appendList(request.getMiddleNames(), "middleName").appendList(request.getPersonDbIds(), "id")
6363
.appendList(request.getPhoneNumbers(), "phoneNumber").appendList(request.getUserIDs(), "userID");
6464

65-
Page<PersonEntity> entityPage = peopleRepository.findAllBySearch(searchQuery, pageReq);
65+
Page<PersonEntity> entityPage = peopleRepository.findAllBySearchAndPaginate(searchQuery, pageReq);
6666

6767
List<Person> data = entityPage.map(this::convertToPerson).getContent();
6868
PagingUtility.calculateMetaData(metadata, entityPage);

src/main/java/org/brapi/test/BrAPITestServer/service/core/ProgramService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public List<Program> findPrograms(ProgramSearchRequest request, Metadata metadat
6969
.appendList(request.getObjectives(), "objective").appendList(request.getProgramDbIds(), "id")
7070
.appendList(request.getProgramNames(), "name").appendEnumList(request.getProgramTypes(), "programType");
7171

72-
Page<ProgramEntity> page = programRepository.findAllBySearch(searchQuery, pageReq);
72+
Page<ProgramEntity> page = programRepository.findAllBySearchAndPaginate(searchQuery, pageReq);
7373
List<Program> programs = page.map(this::convertFromEntity).getContent();
7474
PagingUtility.calculateMetaData(metadata, page);
7575
return programs;

src/main/java/org/brapi/test/BrAPITestServer/service/core/SeasonService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public List<Season> findSeasons(String seasonDbId, String season, String seasonN
4444
if (year != null)
4545
searchQuery = searchQuery.appendSingle(year, "year");
4646

47-
Page<SeasonEntity> entityPage = seasonRepository.findAllBySearch(searchQuery, pageReq);
47+
Page<SeasonEntity> entityPage = seasonRepository.findAllBySearchAndPaginate(searchQuery, pageReq);
4848

4949
List<Season> data = entityPage.map(this::convertFromEntity).getContent();
5050
PagingUtility.calculateMetaData(metadata, entityPage);

src/main/java/org/brapi/test/BrAPITestServer/service/core/StudyService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public List<Study> findStudies(StudySearchRequest request, Metadata metaData) {
149149
.appendList(request.getTrialDbIds(), "trial.id").appendList(request.getTrialNames(), "trial.trialName")
150150
.withSort(getSortByField(request.getSortBy()), request.getSortOrder());
151151

152-
Page<StudyEntity> studiesPage = studyRepository.findAllBySearch(searchQuery, pageReq);
152+
Page<StudyEntity> studiesPage = studyRepository.findAllBySearchAndPaginate(searchQuery, pageReq);
153153
PagingUtility.calculateMetaData(metaData, studiesPage);
154154

155155
List<Study> studies = studiesPage.map(this::convertFromEntity).getContent();

0 commit comments

Comments
 (0)