hibernate - JPQL 和元组列表作为 SELECT IN 语句的参数

标签 hibernate spring-boot jpa kotlin jpql

给定以下表格布局:

CREATE TABLE things (
    id      BIGINT PRIMARY KEY NOT NULL,
    foo     BIGINT NOT NULL,
    bar     BIGINT NOT NULL
);

实体类 (Kotlin):

@Entity
@Table(name = "things")
class Thing(
        val foo: Long,
        val bar: Long
) : AbstractPersistable<Long>()

还有一个存储库:

interface ThingRepository : JpaRepository<Thing, Long> {
@Query("SELECT t FROM Thing t WHERE t.foo IN ?1")
fun selectByFoos(foos: Iterable<Long>): Iterable<Thing>

@Query("SELECT t FROM Thing t WHERE (t.foo, t.bar) IN ((1, 2), (3, 4))")
fun selectByFoosAndBarsFixed(): Iterable<Thing>

@Query("SELECT t FROM Thing t WHERE (t.foo, t.bar) IN ?1")
fun selectByFoosAndBars(foosAndBars: Iterable<Pair<Long, Long>>): Iterable<Thing>

以下两个调用工作正常:

repo.selectByFoos(listOf(1L, 3L))
repo.selectByFoosAndBarsFixed()

但是这个没有:

repo.selectByFoosAndBars(listOf(Pair(1L, 2L), Pair(3L, 4L)))

它抛出:

org.springframework.dao.DataIntegrityViolationException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not extract ResultSet

Caused by: org.h2.jdbc.JdbcSQLException: Data conversion error converting "aced00057372000b6b6f746c696e2e50616972fa1b06813de78f780200024c000566697273747400124c6a6176612f6c616e672f4f626a6563743b4c00067365636f6e6471007e000178707372000e6a6176612e6c616e672e4c6f6e673b8be490cc8f23df0200014a000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000000000000017371007e00030000000000000002"; SQL statement:
/* SELECT t FROM Thing t WHERE (t.foo, t.bar) IN ?1 */ select thing0_.id as id1_0_, thing0_.bar as bar2_0_, thing0_.foo as foo3_0_ from things thing0_ where (thing0_.foo , thing0_.bar) in (? , ?) [22018-197]

Caused by: java.lang.NumberFormatException: For input string: "aced00057372000b6b6f746c696e2e50616972fa1b06813de78f780200024c000566697273747400124c6a6176612f6c616e672f4f626a6563743b4c00067365636f6e6471007e000178707372000e6a6176612e6c616e672e4c6f6e673b8be490cc8f23df0200014a000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000000000000017371007e00030000000000000002"

我想作为参数传递的列表元素没有正确插入到查询中。我该如何纠正这个问题?

当然,我可以像这样手动构建查询:

@Repository
class SecondThingRepository(private val entityManager: EntityManager) {
    fun selectByFoosAndBars(foosAndBars: Iterable<Pair<Long, Long>>): Iterable<Thing> {
        val pairsRepr = foosAndBars.joinToString(prefix = "(", postfix = ")") { "(${it.first}, '${it.second}')" }
        val query: TypedQuery<Thing> = entityManager.createQuery("SELECT t FROM Thing t WHERE (t.foo, t.bar) IN $pairsRepr", Thing::class.java)
        return query.resultList
    }
}

不过这个好像note很好。

最佳答案

首先,提醒一句:并非所有数据库都支持 (t.foo, t.bar) IN ((1, 2), (3, 4)) 语法。使用它会使您的应用程序不可移植。

我假设 ListPair 的数量可以是任意的(如果不是,有一个更简单的解决方案涉及修改 IN 表达式到例如 IN (?1, ?2, ?3) 并更新查询方法以接受 List 类型的三个参数。我想那不是什么不过,您要的是)。

问题是 Hibernate 不知道如何将 Pair 类映射到数据库类型。似乎集合元素的类型解析逻辑与“外部”类型的解析逻辑不同,所以 listOf(listOf(1L, 2L), listOf(3L, 4L)) 不会也不行。

解决方案(请注意,这有点 hack)是引入一个 Hibernate 的 UserType 能够映射 Pair 对象并使用这个新创建的 PairType 用于 List 的元素。

首先,将以下类添加到您的项目中:

/* It is absolutely crucial that this class extend Pair. If the Pair class you're using
happens to be final, you will have to implement a Pair class yourself. 
For an explanation of why this is required, have a look at SessionFactory.resolveParameterBindType()
and TypeResolver.heuristicType() methods */
public class PairType extends Pair<Long, Long> implements UserType { 

    public PairType(Long first, Long second) {
        super(first, second);
    }

    public PairType() {
        super(null, null);
    }

    @Override
    public int[] sqlTypes() {
        return new int[] {Types.ARRAY};
    }

    @Override
    public Class returnedClass() {
        return Pair.class;
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
            throws HibernateException, SQLException {
        if (Objects.isNull(value)) {
            st.setNull(index, Types.ARRAY);
        } else {
            final Pair pair = (Pair) value;
            st.setArray(index, new Array() {


                @Override
                public Object getArray() throws SQLException {
                    // TODO Auto-generated method stub
                    return new Object[] {pair.getFirst(), pair.getSecond()};
                }

                ...
                //you can leave the rest of the autogenerated method stubs as they are

            });
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        if (Objects.isNull(value)) {
            return null;
        }
        return Pair.of(((Pair) value).getFirst(), ((Pair) value).getSecond());
    }

    @Override
    public boolean isMutable() {
        return false;
    }

    ...
    //you can leave the rest of the autogenerated method stubs as they are here as well

} 

然后,将您的方法签名修改为:

selectByFoosAndBars(foosAndBars: Iterable<PairType>): Iterable<Thing>

注意:上述解决方案对我来说开箱即用,适用于 H2 数据库。你的旅费可能会改变。

关于hibernate - JPQL 和元组列表作为 SELECT IN 语句的参数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55317347/

相关文章:

java - 使用继承的 JPA OneToMany

java - Hibernate 实体中的 Postgres 数组

java - Spring JPA 与 Hibernate 财务序列生成

java - 从应用程序获取 Hibernate session 计数

java - Spring Boot 正在缓存结果而不启用它

java - 通过cURL(json)显示消息(Spring Boot)

java - AutoConfigureTestDatabase 已弃用

java - CriteriaBuilder 连接/子查询与结果整数

java - 如何在我的 Spring WebFlow 项目中向这个 hibernate 调用添加排序

java - 多对多关系的jpa标准