in genie-web/src/main/java/com/netflix/genie/web/data/services/impl/jpa/JpaPersistenceServiceImpl.java [518:634]
public Page<Cluster> findClusters(
@Nullable final String name,
@Nullable final Set<ClusterStatus> statuses,
@Nullable final Set<String> tags,
@Nullable final Instant minUpdateTime,
@Nullable final Instant maxUpdateTime,
final Pageable page
) {
/*
* NOTE: This is implemented this way for a reason:
* 1. To solve the JPA N+1 problem: https://vladmihalcea.com/n-plus-1-query-problem/
* 2. To address this: https://vladmihalcea.com/fix-hibernate-hhh000104-entity-fetch-pagination-warning-message/
* This reduces the number of queries from potentially 100's to 3
*/
log.debug(
"[findClusters] Called with name = {}, statuses = {}, tags = {}, minUpdateTime = {}, maxUpdateTime = {}",
name,
statuses,
tags,
minUpdateTime,
maxUpdateTime
);
final Set<String> statusStrings = statuses != null
? statuses.stream().map(Enum::name).collect(Collectors.toSet())
: null;
// TODO: Still more optimization that can be done here to not load these entities
// Figure out how to use just strings in the predicate
final Set<TagEntity> tagEntities = tags == null
? null
: this.tagRepository.findByTagIn(tags);
if (tagEntities != null && tagEntities.size() != tags.size()) {
// short circuit for no results as at least one of the expected tags doesn't exist
return new PageImpl<>(new ArrayList<>(0));
}
final CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
final CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
final Root<ClusterEntity> countQueryRoot = countQuery.from(ClusterEntity.class);
final Subquery<Long> countIdSubQuery = countQuery.subquery(Long.class);
final Root<ClusterEntity> countIdSubQueryRoot = countIdSubQuery.from(ClusterEntity.class);
countIdSubQuery.select(countIdSubQueryRoot.get(ClusterEntity_.id));
countIdSubQuery.where(
ClusterPredicates.find(
countIdSubQueryRoot,
countIdSubQuery,
criteriaBuilder,
name,
statusStrings,
tagEntities,
minUpdateTime,
maxUpdateTime
)
);
countQuery.select(criteriaBuilder.count(countQueryRoot));
countQuery.where(countQueryRoot.get(ClusterEntity_.id).in(countIdSubQuery));
final Long totalCount = this.entityManager.createQuery(countQuery).getSingleResult();
if (totalCount == null || totalCount == 0) {
// short circuit for no results
return new PageImpl<>(new ArrayList<>(0));
}
final CriteriaQuery<Long> idQuery = criteriaBuilder.createQuery(Long.class);
final Root<ClusterEntity> idQueryRoot = idQuery.from(ClusterEntity.class);
idQuery.select(idQueryRoot.get(ClusterEntity_.id));
idQuery.where(
// NOTE: The problem with trying to reuse the predicate above even though they seem the same is they have
// different query objects. If there is a join added by the predicate function it won't be on the
// right object as these criteria queries are basically builders
ClusterPredicates.find(
idQueryRoot,
idQuery,
criteriaBuilder,
name,
statusStrings,
tagEntities,
minUpdateTime,
maxUpdateTime
)
);
final Sort sort = page.getSort();
final List<Order> orders = new ArrayList<>();
sort.iterator().forEachRemaining(
order -> {
if (order.isAscending()) {
orders.add(criteriaBuilder.asc(idQueryRoot.get(order.getProperty())));
} else {
orders.add(criteriaBuilder.desc(idQueryRoot.get(order.getProperty())));
}
}
);
idQuery.orderBy(orders);
final List<Long> clusterIds = this.entityManager
.createQuery(idQuery)
.setFirstResult(((Long) page.getOffset()).intValue())
.setMaxResults(page.getPageSize())
.getResultList();
final CriteriaQuery<ClusterEntity> contentQuery = criteriaBuilder.createQuery(ClusterEntity.class);
final Root<ClusterEntity> contentQueryRoot = contentQuery.from(ClusterEntity.class);
contentQuery.select(contentQueryRoot);
contentQuery.where(contentQueryRoot.get(ClusterEntity_.id).in(clusterIds));
// Need to make the same order by or results won't be accurate
contentQuery.orderBy(orders);
final List<Cluster> clusters = this.entityManager
.createQuery(contentQuery)
.setHint(LOAD_GRAPH_HINT, this.entityManager.getEntityGraph(ClusterEntity.DTO_ENTITY_GRAPH))
.getResultStream()
.map(EntityV4DtoConverters::toV4ClusterDto)
.collect(Collectors.toList());
return new PageImpl<>(clusters, page, totalCount);
}