使用 Spring JPA 规范按子查询排序

Posted

技术标签:

【中文标题】使用 Spring JPA 规范按子查询排序【英文标题】:Order by subquery with Spring JPA Specification 【发布时间】:2018-07-22 08:56:41 【问题描述】:

我有一个排序问题,本机 SQL 和可能 JPQL 很容易解决这个问题,但 Spring JPA 规范却没有。

这是修剪后的域模型:

@Entity
class DesignElement 
   List<Change> changes;

@Entity
class Change 
   List<DesignElement> designElements;

@Entity
class Vote 
   Change change;
   VoteType type;

用户在几个元素上发布他们的Change 提案,其他用户使用VoteTypeGREATGOODNEUTRALBAD 对这些更改进行投票。

我试图完成的是通过GOODGREAT 的票数来订购Entitys(而不是Changes)。

使用 SQL 或 JPQL 可能更容易,但我们尝试使用 Specification API,因为我们需要其他几个谓词和顺序。

经过两天的痛苦,我想出了几个关于我不能用 Specification api 做什么的假设。其中一些可能是错误的,如果是这样,我将用答案编辑问题。

如果我们有多个 Specification&lt;DesignElement&gt;s 要组合,我们不能只在根查询中添加 count(Vote_.id)。 在 JPA 中无法通过子查询进行排序:https://hibernate.atlassian.net/browse/HHH-256 不能在子查询中使用multiselect

这是在其他Specification&lt;DesignElement&gt;s 中定义的其他几个工作Orders:

private static void appendOrder(CriteriaQuery<?> query, Order order) 
    List<Order> orders = new ArrayList<>(query.getOrderList());
    orders.add(0, order);
    query.orderBy(orders);


public static Specification<DesignElement> orderByOpen() 
    return new Specification<DesignElement>() 
        @Override
        public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) 
            appendOrder(query, cb.desc(root.get("status").get("open")));
            return null;
        
    ;


public static Specification<DesignElement> orderByReverseSeverityRank() 
    return new Specification<DesignElement>() 
        @Override
        public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) 
            appendOrder(query, cb.desc(root.get("severity").get("rank")));
            return null;
        
    ;


public static Specification<DesignElement> orderByCreationTime() 
    return new Specification<DesignElement>() 
        @Override
        public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) 
            appendOrder(query, cb.asc(root.get("creationTime"));
            return null;
        
    ;

这将允许这样的用法:

List<DesignElement> elements = designElementDao.findAll(Specifications.where(orderByReverseSeverityRank()).and(orderByCreationTime());

【问题讨论】:

我正在尝试使用 Spring JPA 规范 API:docs.spring.io/spring-data/jpa/docs/current/api/org/… 【参考方案1】:

我遇到了同样的问题。对于按 count(child-collection) 排序,我找到了一种解决方法 - 它并不理想(污染实体对象,不是动态等),但在某些情况下,它可能就足够了。

    定义只读字段“upvotesCount”,保存用于排序的信息

    @Entity
    class Change 
    
        List<DesignElement> designElements;
    
        // let's define read-only field, mapped to db view 
        @Column(table = "change_votes_view", name = "upvotes_count", updatable = false, insertable = false)
        Integer upvotesCount;
    
    
    

    实现字段映射到的数据库视图

    CREATE VIEW change_votes_view AS SELECT c.id, count(v.*) as upvotes_count
    FROM change c
    LEFT JOIN votes v 
      ON <ids-condition> AND <votes-are-up=condition>
    

另一种方法是使用 DTO 对象 - 您可以构建符合您需求的查询,并且您也可以动态地执行此操作。

【讨论】:

是的,views 几乎总是适用于这样的情况,但我们正在努力避免它们。 实体还需要添加@SecondaryTable(name = "change_votes_view")【参考方案2】:

我想出了一个有点老套的解决方案。

还有一个问题,在 H2 单元测试数据库中并不明显,但在实际的 Postgres 安装中出现:如果直接在 Order by 子句中使用,则必须按列分组:must appear in the GROUP BY clause or be used in an aggregate function

解决方案涉及两种不同的方法:

构造连接,以免出现重复行并且只需对这些单行执行标识聚合操作(如 sum

或者

用感兴趣的重复行构造连接count子ID的数量。 (别忘了group by root id)。

计算子 ID 的数量是解决此问题的方法。但即便如此(在感兴趣的行上创建先决条件)证明是困难的(我很确定这是可能的),所以我采取了一种有点老套的方法:交叉加入所有孩子,并在 case 上做一个sum

public static Specification<DesignElement> orderByGoodVotes() 
    return new Specification<DesignElement>() 
        @Override
        public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) 

            ListAttribute<? super DesignElement, Alert> designElementChanges = root.getModel().getList("changes", Change.class);
            ListJoin<Change, Vote> changeVotes = root.join(designElementChanges).joinList("votes", JoinType.LEFT);
            Path<VoteType> voteType = changeVotes.get("type");

            // This could have been avoided with a precondition on Change -> Vote join
            Expression<Integer> goodVotes1_others0 = cb.<Integer>selectCase().when(cb.in(voteType).value(GOOD).value(GREAT, 1).otherwise(0);

            Order order = cb.desc(cb.sum(goodVotes1_others0));
            appendOrder(query, order);

            // This is required
            query.groupBy(root.get("id"));
            return null;
        
    ;

我概括了这种方法并修正了之前的规范:

public static Specification<DesignElement> orderByOpen() 
    return new Specification<DesignElement>() 
        @Override
        public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) 
            Expression<Integer> opens1_others0 = cb.<Integer>selectCase().when(cb.equal(root.get("status").get("open"), Boolean.TRUE), 1).otherwise(0);
            appendOrder(query, cb.desc(cb.sum(opens1_others0)));
            query.groupBy(root.get("id"));
            return null;
        
    ;


public static Specification<DesignElement> orderByReverseSeverityRank() 
    return new Specification<DesignElement>() 
        @Override
        public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) 
            appendOrder(query, cb.asc(cb.sum(root.get("severity").get("rank"))));
            query.groupBy(root.get("id"));
            return null;
        
    ;


public static Specification<DesignElement> orderByCreationTime() 
    return new Specification<DesignElement>() 
        @Override
        public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) 
            // This isn't changed, we are already selecting all the root attributes
            appendOrder(query, cb.desc(root.get("creationTime")));
            return null;
        
    ;

由于还有大约 15 个其他 Specifications 像这样,我想这么懒惰会导致性能损失,但我没有分析它。

【讨论】:

以上是关于使用 Spring JPA 规范按子查询排序的主要内容,如果未能解决你的问题,请参考以下文章

Mysql:按子查询中的最大 N 个值排序

使用带有实体框架的动态字段按子记录排序

Spring Data JPA:创建规范查询获取连接

从所有帖子中获取 ACF 转发器字段值,按子字段排序

Laravel Query Builder 在按 ID 分组的查询中按子查询减去 COUNT

无法在 FROM 子句中使用子查询编写 JPQL 查询 - Spring Data Jpa - Hibernate