在Hibernate JPA 2中使用子查询进行ORDER BY

EJJ*_*EJJ 5 hibernate jpa-2.0

我正在hibernate-jpa-2.1中将NamedQuery重写为CriteriaQuery。原始的NamedQuery包含一个order by子句,该子句引用别名的子查询。

select new ItemDto ( item.id, item.number, (select count(*) from ClickEntity as click where click.item.id = item.id) as clickCount ) from ItemEntity as item order by clickCount desc

我找不到任何使用别名来引用clickCount字段的方法,因此我认为我也可以在两个地方都使用子查询:

public List<ItemDto> getItems() {
    ...
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<ItemDto> query = criteriaBuilder.createQuery(ItemDto.class);
    Root<ItemEntity> item = query.from(ItemEntity.class);

    query
        .select(
            cb.construct(ItemDto.class,
                item.get("id"),
                item.get("number"),
                getClickCount(cb, query, item).getSelection()
            )
        )
        .orderBy(cb.desc(getClickCount(cb, query, item).getSelection()))

    TypedQuery<ItemDto> typedQuery = entityManager.createQuery(query);
    return typedQuery.getResultList();
}

private Subquery<Long> getClickCount(CriteriaBuilder cb, CriteriaQuery<ItemDto> query, Root<ItemEntity> item) {
    Subquery<Long> subquery = query.subquery(Long.class);
    Root<ClickEntity> click = subquery.from(ClickEntity.class)

    return subquery
        .select(cb.count(click.get("id")))
        .where(cb.equal(click.get("item").get("id"), item.get("id")));
}
Run Code Online (Sandbox Code Playgroud)

但是,当调用getItems()时,Hibernate在创建TypedQuery时引发以下异常:

org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected AST node: query [...]
Run Code Online (Sandbox Code Playgroud)

解析的查询如下所示:

select new ItemDto(
    generatedAlias0.id,
    generatedAlias0.number,
    (select count(generatedAlias1.id) from ClickEntity as generatedAlias1 where( generatedAlias1.item.id=generatedAlias0.id ))
)

from ItemEntity as generatedAlias0 

order by
    (select count(generatedAlias2.id) from ClickEntity as generatedAlias2 where( generatedAlias2.item.id=generatedAlias0.id )) desc
Run Code Online (Sandbox Code Playgroud)

尽管抛出了错误,但此查询对我来说还是不错的。我已经在不使用order by子句的情况下对其进行了测试,然后它可以按预期工作,因此错误肯定是由该子句引起的。但是,由于Subquery显然可以正常工作,因此我很难弄清问题所在。

我尝试过/考虑过的内容:

  • 使用@PostConstruct设置ItemEntity的@Transient字段;这不是一个选项,因为在实际应用程序中,clickCount的值取决于Date参数。
  • 检索结果后订购;这不是一个选项,因为在应用(可选)限制参数之前需要进行排序
  • 不使用getSelection()。这具有相同的效果(甚至是相同的查询)。

因此,我想知道,Hibernate实际上支持这种方法吗?或者我是否缺少(可能更简单)将子查询结果用作排序参数的替代方法?

EJJ*_*EJJ 1

我找到了解决这个问题的两种选择,这两种选择都会产生不同的结果。请注意,由于 select 子句中使用了聚合函数,因此两者都需要为未通过聚合选择的每个列使用group by子句。

1. 使用 where 子句进行交叉连接

为查询创建额外的根将导致交叉联接。与 where 子句结合使用,这将导致内部联接,同时您仍然可以访问根中的字段。添加更多 where 子句可以进行进一步过滤。

public List<ItemDto> getItems() {
    ...
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<ItemDto> query = criteriaBuilder.createQuery(ItemDto.class);
    Root<ItemEntity> item = query.from(ItemEntity.class);
    //Extra root here
    Root<ClickEntity> click = query.from(ClickEntity.class);

    query
        .select(
            cb.construct(ItemDto.class,
                item.get("id"),
                item.get("number"),
                cb.count(click.get("id"))
            )
        )
        //Required to make the cross join into an inner join
        .where(cb.equal(item.get("id"), click.get("item").get("id")))
        //Required because an aggregate function is used in the select clause
        .groupBy(item.get("id"), item.get("number"))
        //Possibility to refer to root 
        .orderBy(cb.count(click.get("id")));
    ...
}
Run Code Online (Sandbox Code Playgroud)

由于这是内部联接,因此此方法仅选择单击表中的单击实体引用的项目实体。换句话说,点击次数为 0 的项目不会被选择。如果需要过滤没有点击的项目,这是一种有效的方法。

2.向ItemEntity添加字段

通过将 @OneToMany 字段添加到引用单击实体的 ItemEntity,可以创建左连接。首先,更新 ItemEntity:

@Entity
public class ItemEntity {
    ...
    @OneToMany(cascade = CascadeType.ALL)
    //The field in the click entity referring to the item
    @JoinColumn(name="itemid")
    private List<ClickEntity> clicks;
    ...
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以让 JPA 为您执行联接并使用联接来引用 ClickEntity 中的字段。此外,您可以使用 join.on(...) 向联接添加额外的条件,并且使用 query.having() 将允许您过滤掉项目,而无需像第一种方法那样点击。

public List<ItemDto> getItems() {
    ...
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<ItemDto> query = criteriaBuilder.createQuery(ItemDto.class);
    Root<ItemEntity> item = query.from(ItemEntity.class);
    //Join on the clicks field. A left join also selects items with 0 clicks.
    Join<ItemEntity, ClickEntity> clicks = item.join("clicks", JoinType.left);
    //Use join.on if you need more conditions to the join
    /*clicks.on(...) */

    query
        .select(
            cb.construct(ItemDto.class,
                item.get("id"),
                item.get("number"),
                cb.count(clicks.get("id"))
            )
        )
        //Required because an aggregate function is used in the select clause
        .groupBy(item.get("id"), item.get("number"))
        //Uncomment to filter out items without clicks
        /* .having(cb.gt(cb.count(clicks.get("id")), 0)) */
        //Refer to the join
        .orderBy(cb.count(clicks.get("id")));
    ...
}
Run Code Online (Sandbox Code Playgroud)

请注意不要内联 clicks 变量,因为这将有效地将 clicks 表连接到 items 表上两次

最后,第二种方法最适合我的情况,因为我也希望获得无需点击的项目,并且找不到直接的方法将交叉连接转换为左外连接。