使用 NamedNativeQuery 时,Hibernate 不会初始化嵌套实体

Hibernate is not initialising nested entities when using NamedNativeQuery

挑战:

我正在尝试从我的数据库中批量获取一组嵌套实体。生成的数据集范围包含数千个实体,因此我的方法是基于 this post 以分页方式获取实体。数据是从 web-based 前端获取的,纯 un-paged 查询最多需要 10 秒(不可接受)。

问题:

正确获取了“parent”实体,但似乎未获取“child”实体。在 TestRepository.getRankedTests(...) 的结果实体列表中,“child”实体列表未初始化,访问它们将导致 LazyInitializationException。这指出了我的 SqlResultMapping 问题的方向,但我看不到错误。我试图为 child 向我的 SqlResultMapping 注入错误,这导致休眠在运行时抱怨,所以它似乎试图将我的配置映射到 child 的属性实体,尽管 child 个实体的未初始化集合让我目瞪口呆。

Parent实体(Test.kt):

@NamedNativeQuery(
    name = "Test.getRankedTests",
    query = """
        select *
        from (
            select
                *,
                DENSE_RANK() over (
                    order by "o.id"
                ) rank
            from (
                select
                    o.id as "o.id",
                    o.version as "o.version",
                    a.id as "a.id",
                    a.organisation_id as "a.organisation_id",
                    a.type as "a.type"
                from  organisation o
                left join address a on o.id = a.organisation_id
                order by o.organisation_number
            ) o_a_an_c
        ) o_a_an_c_r
        where o_a_an_c_r.rank > :min and o_a_an_c_r.rank <= :max
        """,
    resultSetMapping = "TestMapping"
)
@SqlResultSetMapping(
    name = "TestMapping",
    entities = [
        EntityResult(
            entityClass = Test::class,
            fields = [
                FieldResult(name = "id", column = "o.id"),
                FieldResult(name = "version", column = "o.version"),
            ]
        ),
        EntityResult(
            entityClass = TestChild::class,
            fields = [
                FieldResult(name = "id", column = "a.id"),
                FieldResult(name = "organisation", column = "a.organisation_id"),
            ]
        ),
    ]
)
@Entity
@Table(name = "organisation")
class Test(
    @Id
    val id: Long,
    val version: Long,
    @OneToMany(mappedBy = "organisation", cascade = [CascadeType.ALL], orphanRemoval = true)
    val addresses: List<TestChild>,
)

Child实体(TestChild.kt):

@Entity
@Table(name = "address")
@Suppress("LongParameterList")
class TestChild(
    @Id
    val id: Long,
    @ManyToOne(fetch = FetchType.LAZY)
    val organisation: Test,
)

存储库(TestRepository.kt):

@Repository
interface TestRepository : JpaRepository<Test, Long> {
    fun getRankedTests(
        min: Long,
        max: Long
    ): List<Test>
}

据我所知,无法通过 JPA 结果集映射注释获取集合。如果您愿意,可以使用特定于 Hibernate 的 API 来执行此操作,这看起来类似于:

SQLQuery q = session.createNativeQuery(...);
q.addRoot("o", Test.class)
 .addProperty("id", "o.id")
 .addProperty("version", "o.version");
q.addFetch("a", "o", "addresses")
 .addProperty("id", "a.id")
 .addProperty("organisation", "a.organisation_id");

但是,如果您只是想要高效的分页,我建议您查看 Blaze-Persistence which comes with a specialized implementation and spring-data integration 的方法:

@Repository
interface TestRepository : JpaRepository<Test, Long> {
    @EntityGraph("addresses")
    fun getRankedTests(
        pageable: Pageable
    ): Page<Test>
}

感谢 Christian Beikov 提出的好建议。这里缺少的 link 是 ResultTransformer。由于本机查询最终会在同一 JDBC 行中同时包含父项和子项,因此我们最终会得到一个包含两者的对象数组。 ResultTransformer 将负责将该对象数组映射回实体层次结构。这是我修复它的方法:

添加了用于使用 entityManager 获取结果的 DAO:

@Repository
class Dao(
    @PersistenceContext
    private val entityManager: EntityManager
) {

    fun getRankedTests(): List<Test> =
        entityManager.createNamedQuery("Test.getRankedTests")
            .setParameter("max", 5)
            .setHint(QueryHints.HINT_READONLY, true)
            .unwrap(NativeQuery::class.java)
            .setResultTransformer(TestResultTransformer(entityManager))
            .resultList.filterIsInstance(Test::class.java)
}

创建了以下 ResultTransformer:

class TestResultTransformer(private val entityManager: EntityManager) : BasicTransformerAdapter() {
    override fun transformList(
        list: List<*>
    ): List<Test> {
        val identifiableMap: MutableMap<Long, Test> = mutableMapOf()
        for (entityArray in list) {
            if (entityArray is Array<*>) {
                var test: Test? = null
                var testChild: TestChild? = null
                for (tuple in entityArray) {
                    entityManager.detach(tuple);
                    when (tuple) {
                        is Test -> test = tuple
                        is TestChild -> testChild = tuple
                        else -> {
                            throw UnsupportedOperationException(
                                "Tuple " + tuple?.javaClass + " is not supported!"
                            )
                        }
                    }
                }
                if (test != null) {
                    val key = test.id
                    if (!identifiableMap.containsKey(key)) {
                        identifiableMap[key] = test
                        test.addresses = mutableListOf()
                    }
                    if (testChild != null) {
                        test.addresses.add(testChild)
                    }
                }
            }
        }
        return identifiableMap.values.toList()
    }
}