JPA 继承@EntityGraph 包括子类的可选关联
JPA inheritance @EntityGraph include optional associations of subclasses
给定以下域模型,我想加载所有 Answer
,包括它们的 Value
及其各自的子子项,并将其放入 AnswerDTO
中,然后转换为JSON。我有一个可行的解决方案,但它遇到了 N+1 问题,我想通过使用临时 @EntityGraph
来解决这个问题。所有关联都配置为 LAZY
.
@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();
在 Repository
方法上使用临时 @EntityGraph
我可以确保预取值以防止 Answer->Value
关联上的 N+1。虽然我的结果很好,但还有另一个 N+1 问题,因为延迟加载 MCValue
的 selected
关联。
使用这个
@EntityGraph(attributePaths = {"value.selected"})
失败,因为 selected
字段当然只是某些 Value
实体的一部分:
Unable to locate Attribute with the the given name [selected] on this ManagedType [x.model.Value];
如果值是 MCValue
,我如何告诉 JPA 只尝试获取 selected
关联?我需要像 optionalAttributePaths
.
这样的东西
我不知道 Spring-Data 在那里做什么,但要做到这一点,您通常必须使用 TREAT
运算符才能访问子关联,但是该运营商的实施非常有问题。
Hibernate 支持隐式子类型 属性 访问,这是您在这里需要的,但显然 Spring-Data 无法正确处理。我可以建议您看一下 Blaze-Persistence Entity-Views,这是一个在 JPA 之上运行的库,它允许您将任意结构映射到您的实体模型。您可以以类型安全的方式映射您的 DTO 模型,也可以映射继承结构。您的用例的实体视图可能如下所示
@EntityView(Answer.class)
interface AnswerDTO {
@IdMapping
Long getId();
ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
@IdMapping
Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
@Mapping("selected.id")
Set<Long> getOption();
}
借助 Blaze-Persistence 提供的 spring 数据集成,您可以像这样定义存储库并直接使用结果
@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
List<AnswerDTO> findAll();
}
它将生成一个 HQL 查询,只选择您在 AnswerDTO
中映射的内容,如下所示。
SELECT
a.id,
v.id,
TYPE(v),
CASE WHEN TYPE(v) = TextValue THEN v.text END,
CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s
我的最新项目使用了 GraphQL(对我来说是第一次),我们在 N+1 查询和尝试优化查询以仅在需要时才加入表时遇到了一个大问题。我找到了 Cosium
/
spring-data-jpa-entity-graph无可替代。它扩展了 JpaRepository
并添加了将实体图传递给查询的方法。然后,您可以在运行时构建动态实体图,以便仅为您需要的数据添加左连接。
我们的数据流看起来像这样:
- 接收 GraphQL 请求
- 解析 GraphQL 请求并转换为查询中的实体图节点列表
- 从发现的节点创建实体图并传递到存储库中执行
为了解决实体图中不包含无效节点的问题(例如来自 graphql 的 __typename
),我创建了一个实用程序 class 来处理实体图的生成。调用 class 传入它为其生成图形的 class 名称,然后根据 ORM 维护的元模型验证图形中的每个节点。如果该节点不在模型中,则将其从图形节点列表中删除。 (此检查需要递归并检查每个 child)
在找到这个之前,我尝试了投影和 Spring JPA / Hibernate 文档中推荐的所有其他替代方案,但似乎没有任何东西可以优雅地或至少使用大量额外代码解决问题
根据您的评论进行编辑:
抱歉,我在第一轮没有理解你的问题,你的问题发生在 spring-data 启动时,而不仅仅是当你尝试调用 findAll() 时。
所以,您现在可以浏览完整的示例,可以从我的 github 中提取:
https://github.com/bdzzaid/Whosebug-java/blob/master/jpa-hibernate/
您可以在此项目中轻松重现和修复您的问题。
Effectivly,Spring数据和hibernate默认无法确定"selected"图,您需要指定收集所选选项的方式。
所以首先,您必须声明 class Answer
的 NamedEntityGraphs
如您所见,class 的属性 value 有两个 NamedEntityGraph回答
第一个为所有值无特定关系加载
第二个为特定的 Multichoice 值。如果你删除这个,你会重现异常。
其次,如果要获取 LAZY[=44= 类型的数据,则需要在事务上下文中 answerRepository.findAll() ]
@Entity
@Table(name = "answer")
@NamedEntityGraphs({
@NamedEntityGraph(
name = "graph.Answer",
attributeNodes = @NamedAttributeNode(value = "value")
),
@NamedEntityGraph(
name = "graph.AnswerMultichoice",
attributeNodes = @NamedAttributeNode(value = "value"),
subgraphs = {
@NamedSubgraph(
name = "graph.AnswerMultichoice.selected",
attributeNodes = {
@NamedAttributeNode("selected")
}
)
}
)
}
)
public class Answer
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(updatable = false, nullable = false)
private int id;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "value_id", referencedColumnName = "id")
private Value value;
// ..
}
如果关联属性是超class的一部分并且也是所有子class的一部分,则您只能使用EntityGraph
。否则,EntityGraph
将始终失败并显示您当前获得的 Exception
。
避免 N+1 select 问题的最佳方法是将查询拆分为 2 个查询:
第一个查询使用 EntityGraph
获取 MCValue
实体,以获取由 selected
属性映射的关联。在该查询之后,这些实体将存储在 Hibernate 的一级缓存/持久性上下文中。 Hibernate 将在处理第二个查询的结果时使用它们。
@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();
第二个查询然后获取 Answer
实体并使用 EntityGraph
也获取关联的 Value
实体。对于每个 Value
实体,Hibernate 将实例化特定的子 class 并检查一级缓存是否已经包含该 class 和主键组合的对象。如果是这种情况,Hibernate 将使用一级缓存中的对象而不是查询返回的数据。
@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();
因为我们已经获取了所有 MCValue
个具有关联 selected
个实体的实体,所以我们现在得到 Answer
个具有初始化 value
关联的实体。并且如果关联包含一个 MCValue
实体,它的 selected
关联也将被初始化。
给定以下域模型,我想加载所有 Answer
,包括它们的 Value
及其各自的子子项,并将其放入 AnswerDTO
中,然后转换为JSON。我有一个可行的解决方案,但它遇到了 N+1 问题,我想通过使用临时 @EntityGraph
来解决这个问题。所有关联都配置为 LAZY
.
@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();
在 Repository
方法上使用临时 @EntityGraph
我可以确保预取值以防止 Answer->Value
关联上的 N+1。虽然我的结果很好,但还有另一个 N+1 问题,因为延迟加载 MCValue
的 selected
关联。
使用这个
@EntityGraph(attributePaths = {"value.selected"})
失败,因为 selected
字段当然只是某些 Value
实体的一部分:
Unable to locate Attribute with the the given name [selected] on this ManagedType [x.model.Value];
如果值是 MCValue
,我如何告诉 JPA 只尝试获取 selected
关联?我需要像 optionalAttributePaths
.
我不知道 Spring-Data 在那里做什么,但要做到这一点,您通常必须使用 TREAT
运算符才能访问子关联,但是该运营商的实施非常有问题。
Hibernate 支持隐式子类型 属性 访问,这是您在这里需要的,但显然 Spring-Data 无法正确处理。我可以建议您看一下 Blaze-Persistence Entity-Views,这是一个在 JPA 之上运行的库,它允许您将任意结构映射到您的实体模型。您可以以类型安全的方式映射您的 DTO 模型,也可以映射继承结构。您的用例的实体视图可能如下所示
@EntityView(Answer.class)
interface AnswerDTO {
@IdMapping
Long getId();
ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
@IdMapping
Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
@Mapping("selected.id")
Set<Long> getOption();
}
借助 Blaze-Persistence 提供的 spring 数据集成,您可以像这样定义存储库并直接使用结果
@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
List<AnswerDTO> findAll();
}
它将生成一个 HQL 查询,只选择您在 AnswerDTO
中映射的内容,如下所示。
SELECT
a.id,
v.id,
TYPE(v),
CASE WHEN TYPE(v) = TextValue THEN v.text END,
CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s
我的最新项目使用了 GraphQL(对我来说是第一次),我们在 N+1 查询和尝试优化查询以仅在需要时才加入表时遇到了一个大问题。我找到了 Cosium
/
spring-data-jpa-entity-graph无可替代。它扩展了 JpaRepository
并添加了将实体图传递给查询的方法。然后,您可以在运行时构建动态实体图,以便仅为您需要的数据添加左连接。
我们的数据流看起来像这样:
- 接收 GraphQL 请求
- 解析 GraphQL 请求并转换为查询中的实体图节点列表
- 从发现的节点创建实体图并传递到存储库中执行
为了解决实体图中不包含无效节点的问题(例如来自 graphql 的 __typename
),我创建了一个实用程序 class 来处理实体图的生成。调用 class 传入它为其生成图形的 class 名称,然后根据 ORM 维护的元模型验证图形中的每个节点。如果该节点不在模型中,则将其从图形节点列表中删除。 (此检查需要递归并检查每个 child)
在找到这个之前,我尝试了投影和 Spring JPA / Hibernate 文档中推荐的所有其他替代方案,但似乎没有任何东西可以优雅地或至少使用大量额外代码解决问题
根据您的评论进行编辑:
抱歉,我在第一轮没有理解你的问题,你的问题发生在 spring-data 启动时,而不仅仅是当你尝试调用 findAll() 时。
所以,您现在可以浏览完整的示例,可以从我的 github 中提取: https://github.com/bdzzaid/Whosebug-java/blob/master/jpa-hibernate/
您可以在此项目中轻松重现和修复您的问题。
Effectivly,Spring数据和hibernate默认无法确定"selected"图,您需要指定收集所选选项的方式。
所以首先,您必须声明 class Answer
的 NamedEntityGraphs如您所见,class 的属性 value 有两个 NamedEntityGraph回答
第一个为所有值无特定关系加载
第二个为特定的 Multichoice 值。如果你删除这个,你会重现异常。
其次,如果要获取 LAZY[=44= 类型的数据,则需要在事务上下文中 answerRepository.findAll() ]
@Entity
@Table(name = "answer")
@NamedEntityGraphs({
@NamedEntityGraph(
name = "graph.Answer",
attributeNodes = @NamedAttributeNode(value = "value")
),
@NamedEntityGraph(
name = "graph.AnswerMultichoice",
attributeNodes = @NamedAttributeNode(value = "value"),
subgraphs = {
@NamedSubgraph(
name = "graph.AnswerMultichoice.selected",
attributeNodes = {
@NamedAttributeNode("selected")
}
)
}
)
}
)
public class Answer
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(updatable = false, nullable = false)
private int id;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "value_id", referencedColumnName = "id")
private Value value;
// ..
}
如果关联属性是超class的一部分并且也是所有子class的一部分,则您只能使用EntityGraph
。否则,EntityGraph
将始终失败并显示您当前获得的 Exception
。
避免 N+1 select 问题的最佳方法是将查询拆分为 2 个查询:
第一个查询使用 EntityGraph
获取 MCValue
实体,以获取由 selected
属性映射的关联。在该查询之后,这些实体将存储在 Hibernate 的一级缓存/持久性上下文中。 Hibernate 将在处理第二个查询的结果时使用它们。
@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();
第二个查询然后获取 Answer
实体并使用 EntityGraph
也获取关联的 Value
实体。对于每个 Value
实体,Hibernate 将实例化特定的子 class 并检查一级缓存是否已经包含该 class 和主键组合的对象。如果是这种情况,Hibernate 将使用一级缓存中的对象而不是查询返回的数据。
@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();
因为我们已经获取了所有 MCValue
个具有关联 selected
个实体的实体,所以我们现在得到 Answer
个具有初始化 value
关联的实体。并且如果关联包含一个 MCValue
实体,它的 selected
关联也将被初始化。