public Page findClusters()

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);
    }