【问题标题】:Hibernate is not initialising nested entities when using NamedNativeQuery使用 NamedNativeQuery 时,Hibernate 未初始化嵌套实体
【发布时间】:2021-10-12 08:56:32
【问题描述】:

挑战:

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

问题:

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

父实体(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>,
)

子实体(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>
}

【问题讨论】:

    标签: hibernate jpa nativequery sqlresultsetmapping


    【解决方案1】:

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

    添加了一个 DAO 以使用 entityManager 获取结果:

    @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()
        }
    }
    

    【讨论】:

      【解决方案2】:

      AFAIK,无法通过 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 和可以正常工作的 spring-data integration

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

      【讨论】:

      • 感谢反馈,Blaze 提案很有趣,我会研究一下!关于结果集映射,根据vladmihalcea.com/… 应该也可以映射集合。
      • 我昨天设置的时候一定很困,你是对的 Christian Beikov,开箱即用这个映射似乎不起作用。但是,通过提供您自己的 ResultTransformer,您可以映射嵌套集合。
      猜你喜欢
      • 2023-03-13
      • 2020-08-16
      • 2020-03-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多