今天我遇到了 Hibernate 6 和 Firebird 4 数据库的问题,以下查询最好地说明了这一问题:
select * from rdb$relations
where rdb$relation_id in ((select rdb$relation_id from rdb$relations))
执行后出现以下错误:
SQL错误[335544652] [21000]:单例选择中的多行[SQLState:21000,ISC错误代码:335544652]
它的出现是因为 Firebird 内部理解两种形式的 IN() 并以不同的方式处理它们 -
<table subquery>
和 <scalar subquery>
。双括号的使用让 Firebird 将子查询解释为 <scalar subquery>
,它最多只能有一个结果。
以下(示例)查询按预期执行,但不是由 Hibernate 6 生成:
select * from rdb$relations
where rdb$relation_id in (select rdb$relation_id from rdb$relations)
从 Hibernate 5 迁移到 Hibernate 6 后,问题出现在项目中。在从 Criteria API 生成 SQL 时,Hibernate 使用自己的
CriteriaBuilder
接口实现。此实现通过生成适当的 Predicate
实例来处理 IN() 构造。在 Hibernate 5 中,只有 InPredicate
实现。这个在内部正确处理了两种情况 - 值列表和子查询。在 Hibernate 6 中,上述谓词实现已替换为 InListPredicate
和 InSubQueryPredicate
。现在的问题是 Hibernate 6 中的 CriteriaBuilder
实现 - SqmCriteriaNodeBuilder
- 创建了错误的 Predicate
实例 - 它只创建了 InListPredicate
,它在生成的 SQL 中生成双括号。
我目前看到但不太喜欢的唯一一种解决方法是不使用 JPA API,而是使用 Hibernate 特定的 API。所以而不是
public Predicate getFilterPredicate(Root root, CriteriaQuery query, String name, String filterValue, MatchMode matchMode, CriteriaBuilder builder) {
Subquery<Customer> subquery = query.subquery(Customer.class);
Root<Address> from = subquery.from(Address.class);
subquery.select(from.get("customer"));
. . .
return rootQuery.in(subquery);
}
使用以下内容
public Predicate getFilterPredicate(Root root, CriteriaQuery query, String name, String filterValue, MatchMode matchMode, CriteriaBuilder builder) {
Subquery<Customer> subquery = query.subquery(Customer.class);
Root<Address> from = subquery.from(Address.class);
subquery.select(from.get("customer"));
. . .
return ((NodeBuilder)builder).in(rootQuery, (SqmSubQuery)subquery);
}
有人知道如何让 Hibernate 6 创建和使用
InSubQueryPredicate
在我的例子中,同时仍然坚持标准 JPA API 吗?
我能够使用此代码重现问题:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object> q = cb.createQuery();
Root<Customer> cRoot = q.from(Customer.class);
Subquery<Customer> sq = q.subquery(Customer.class);
Root<Address> sqRoot = sq.from(Address.class);
q.where(cRoot.in(sq.select(sqRoot.get("customer"))));
q.select(cRoot);
em.createQuery(q).getResultList()
正如我之前在评论中提到的,JPA 规范仅讨论关于 IN
的
“值列表”,因此它要么不假设使用子查询支持
IN
,要么认为是理所当然的如果您只传递一个值,并且该值是一个子查询表达式,那么它应该起作用。因此,可能是它在 Hibernate 5 中工作的事实是不合规的,或者他们在 Hibernate 6 中所做的更改意外地破坏了 Firebird 的东西,因为 Firebird 的解析器在这一点上不灵活。
无论如何,重写为使用
EXISTS
而不是 IN
可以使其在 JPA API 中工作,而不必求助于转换为 Hibernate 特定的 API:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object> q = cb.createQuery();
Root<Customer> cRoot = q.from(Customer.class);
Subquery<Customer> sq = q.subquery(Customer.class);
Root<Address> sqRoot = sq.from(Address.class);
sq.where(cb.equal(sqRoot.get("customer"), cRoot));
q.where(cb.exists(sq));
q.select(cRoot);
em.createQuery(q).getResultList();
为了复制,我使用了以下虚拟类:
@Entity
@Getter
@Setter
@ToString
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
private String street;
@ManyToOne
@JoinColumn(name = "customer_id", foreignKey = @ForeignKey(name = "CUSTOMER_ID_FK"))
private Customer customer;
}
@Entity
@Getter
@Setter
@ToString
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
private String name;
protected Customer() {
}
public Customer(String name) {
this.name = name;
}
}