使用 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
提案,其他用户使用VoteType
或GREAT
、GOOD
、NEUTRAL
或BAD
对这些更改进行投票。
我试图完成的是通过GOOD
和GREAT
的票数来订购Entity
s(而不是Change
s)。
使用 SQL 或 JPQL 可能更容易,但我们尝试使用 Specification
API,因为我们需要其他几个谓词和顺序。
经过两天的痛苦,我想出了几个关于我不能用 Specification api 做什么的假设。其中一些可能是错误的,如果是这样,我将用答案编辑问题。
如果我们有多个Specification<DesignElement>
s 要组合,我们不能只在根查询中添加 count(Vote_.id)
。
在 JPA 中无法通过子查询进行排序:https://hibernate.atlassian.net/browse/HHH-256
不能在子查询中使用multiselect
。
这是在其他Specification<DesignElement>
s 中定义的其他几个工作Order
s:
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 对象 - 您可以构建符合您需求的查询,并且您也可以动态地执行此操作。
【讨论】:
是的,view
s 几乎总是适用于这样的情况,但我们正在努力避免它们。
实体还需要添加@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 个其他 Specification
s 像这样,我想这么懒惰会导致性能损失,但我没有分析它。
【讨论】:
以上是关于使用 Spring JPA 规范按子查询排序的主要内容,如果未能解决你的问题,请参考以下文章