Hibernate 生成关于带有 IN 子句的子查询的不正确 SQL

问题描述 投票:0回答:1

今天我遇到了 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 吗?

hibernate firebird hibernate-criteria
1个回答
0
投票

我能够使用此代码重现问题:

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

}
© www.soinside.com 2019 - 2024. All rights reserved.