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 问题,因为延迟加载 MCValueselected 关联。

使用这个

@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 并添加了将实体图传递给查询的方法。然后,您可以在运行时构建动态实体图,以便仅为您需要的数据添加左连接。

我们的数据流看起来像这样:

  1. 接收 GraphQL 请求
  2. 解析 GraphQL 请求并转换为查询中的实体图节点列表
  3. 从发现的节点创建实体图并传递到存储库中执行

为了解决实体图中不包含无效节点的问题(例如来自 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 关联也将被初始化。