使用 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()
}
}
挑战:
我正在尝试从我的数据库中批量获取一组嵌套实体。生成的数据集范围包含数千个实体,因此我的方法是基于 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()
}
}