19

给定以下域模型,我想加载所有Answers,包括它们Value的 s 和它们各自的子子项,并将其放入 anAnswerDTO中,然后转换为 JSON。我有一个可行的解决方案,但它遇到了我想通过使用 ad-hoc 摆脱的 N+1 问题@EntityGraph。所有关联均已配置LAZY

在此处输入图像描述

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

@EntityGraph在方法上使用 ad-hocRepository我可以确保预先获取值以防止 N+1 出现Answer->Value关联。虽然我的结果很好,但由于延迟加载 s 的selected关联,还有另一个 N+1 问题MCValue

使用这个

@EntityGraph(attributePaths = {"value.selected"})

失败,因为该selected字段当然只是某些Value实体的一部分:

Unable to locate Attribute  with the the given name [selected] on this ManagedType [x.model.Value];

我如何告诉 JPA 仅selected在值为 a 的情况下尝试获取关联MCValue?我需要类似的东西optionalAttributePaths

4

4 回答 4

10

EntityGraph如果关联属性是超类的一部分并且也是所有子类的一部分,则只能使用。否则,EntityGraph将始终以Exception您当前获得的结果失败。

避免 N+1 选择问题的最佳方法是将查询拆分为 2 个查询:

第一个查询MCValue使用 获取实体,以获取属性EntityGraph映射的关联。selected在该查询之后,这些实体将存储在 Hibernate 的第一级缓存/持久性上下文中。Hibernate 将在处理第二个查询的结果时使用它们。

@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();

然后第二个查询获取Answer实体并使用 anEntityGraph来获取关联的Value实体。对于每个Value实体,Hibernate 将实例化特定的子类并检查第一级缓存是否已经包含该类和主键组合的对象。如果是这种情况,Hibernate 会使用一级缓存中的对象而不是查询返回的数据。

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

因为我们已经获取了MCValue具有关联selected实体的所有实体,所以我们现在获取Answer具有初始化value关联的实体。并且如果关联包含一个MCValue实体,它的selected关联也会被初始化。

于 2020-04-29T13:18:18.337 回答
8

我不知道 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
于 2020-04-17T13:13:48.050 回答
1

我的最新项目使用了 GraphQL(对我来说是第一次),我们在 N+1 查询方面遇到了一个大问题,并试图优化查询以仅在需要时加入表。我发现Cosium / spring-data-jpa-entity-graph 是不可替代的。它扩展JpaRepository并添加了将实体图传递给查询的方法。然后,您可以在运行时构建动态实体图,以便仅为您需要的数据添加左连接。

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

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

为了解决实体图中不包含无效节点的问题(例如__typename来自 graphql),我创建了一个处理实体图生成的实用程序类。调用类传入它为其生成图的类名,然后根据 ORM 维护的元模型验证图中的每个节点。如果节点不在模型中,则将其从图形节点列表中删除。(此检查需要递归并检查每个孩子)

在找到这个之前,我已经尝试过 Spring JPA / Hibernate 文档中推荐的预测和所有其他替代方案,但似乎没有什么可以优雅地解决问题,或者至少需要大量额外的代码

于 2020-04-22T16:45:51.563 回答
0

在您发表评论后编辑:

抱歉,我在第一轮中没有理解您的问题,您的问题发生在 spring-data 启动时,不仅是在您尝试调用 findAll() 时。

因此,您现在可以浏览完整的示例,可以从我的 github 中提取: https ://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

您可以在此项目中轻松重现并修复您的问题。

实际上,Spring 数据和休眠默认情况下无法确定“选定”图形,您需要指定收集选定选项的方式。

所以首先,你必须声明类Answer的 NamedEntityGraphs

可以看到,Answer类的属性值有两个NamedEntityGraph

  • 第一个为所有没有特定关系加载的

  • 第二个用于特定的Multichoice值。如果你删除这个,你会重现异常。

其次,如果要获取LAZY类型的数据,则需要在事务上下文中 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;
// ..
}
于 2020-04-25T06:21:42.437 回答