JPA 继承 @EntityGraph 包括可选的子类关联

Posted

技术标签:

【中文标题】JPA 继承 @EntityGraph 包括可选的子类关联【英文标题】:JPA inheritance @EntityGraph include optional associations of subclasses 【发布时间】:2020-07-29 22:01:59 【问题描述】:

鉴于以下域模型,我想加载所有 Answers,包括它们的 Values 和它们各自的子子级,并将其放入 AnswerDTO 中,然后转换为 JSON。我有一个可行的解决方案,但它遇到了我想通过使用临时@EntityGraph 来解决的 N+1 问题。所有关联都配置LAZY

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

Repository 方法上使用临时@EntityGraph,我可以确保预取值以防止Answer-&gt;Value 关联上的N+1。虽然我的结果很好,但还有另一个 N+1 问题,因为延迟加载了 MCValues 的 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之类的东西。

【问题讨论】:

【参考方案1】:

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

避免 N+1 选择问题的最佳方法是将查询拆分为 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 将实例化特定的子类并检查第一级缓存是否已经包含该类和主键组合的对象。如果是这种情况,Hibernate 会使用一级缓存中的对象,而不是查询返回的数据。

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

因为我们已经获取了所有 MCValue 实体以及关联的 selected 实体,所以我们现在获取了 Answer 实体以及初始化的 value 关联。如果关联包含MCValue 实体,则其selected 关联也将被初始化。

【讨论】:

我想过有两个查询,第一个用于获取答案+值,第二个用于获取 selected 以获取具有 MCValue 的答案。我不喜欢这需要一个额外的循环,并且我需要管理数据集之间的映射。我喜欢您为此利用 Hibernate 缓存的想法。您能否详细说明依赖缓存来包含结果的安全性(就一致性而言)?当在事务中进行查询时,这是否有效?我害怕很难发现和零星的延迟初始化错误。 您需要在同一个事务中执行这两个查询。只要您这样做,并且不清除持久性上下文,它就绝对安全。您的一级缓存将始终包含MCValue 实体。而且您不需要额外的循环。您应该使用 1 个查询获取所有 MCValue 实体,该查询连接到 Answer 并使用与当前查询相同的 WHERE 子句。我也在今天的直播中谈到了这个:youtu.be/70B9znTmi00?t=238它从 3:58 开始,但我在中间回答了一些其他问题...... 太好了,感谢您的跟进!我还想补充一点,这个解决方案每个子类需要 1 个查询。所以可维护性对我们来说还可以,但这个解决方案可能并不适合所有情况。 我需要更正我的最后一条评论:当然,您只需要为每个遇到问题的子类进行查询。另外值得注意的是,对于子类的属性,这似乎不是问题,因为使用了SINGLE_TABLE_INHERITANCE【参考方案2】:

我不知道 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

【讨论】:

嗯,感谢您对我已经找到的库的提示,但我们不会使用它有两个主要原因:1)我们不能依赖 lib 在项目的整个生命周期内得到支持(您的公司 blazebit 相当小,而且还处于起步阶段)。 2) 我们不会使用更复杂的技术栈来优化单个查询。 (我知道您的库可以做更多事情,但我们更喜欢通用技术堆栈,如果没有 JPA 解决方案,我们宁愿实现自定义查询/转换)。 Blaze-Persistence 是开源的,Entity-Views 或多或少是在标准的 JPQL/HQL 之上实现的。它实现的功能是稳定的,并且仍然适用于 Hibernate 的未来版本,因为它在标准之上工作。我知道您不想因为一个用例而介绍某些东西,但我怀疑这是您可以使用实体视图的唯一用例。引入实体视图通常会显着减少样板代码的数量并提高查询性能。如果您不想使用对您有帮助的工具,那就这样吧。 至少你解开了问题并提供了解决方案。因此,即使答案没有解释原始问题中究竟发生了什么以及 JPA 如何解决它,您也会得到赏金。在我看来,JPA 不支持它,它应该成为一个功能请求。我将为仅针对 JPA 的更详细的答案提供另一个赏金。 JPA 根本不可能。您需要 TREAT 运算符,该运算符既不受任何 JPA 提供程序的完全支持,也不受 EntityGraph 注释的支持。因此,您可以对此建模的唯一方法是通过 Hibernate 隐式子类型属性解析功能,这需要您使用显式连接。 在您的回答中,视图定义应为interface MCValueDTO extends ValueDTO @Mapping("selected.id") Set&lt;Long&gt; getOption(); 【参考方案3】:

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

我们的数据流如下所示:

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

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

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

【讨论】:

如何解决从超类型不知道的加载关联的问题?另外,正如另一个答案所说,我们想知道是否有纯 JPA 解决方案,但我也认为 lib 遇到了同样的问题,即 selected 关联不适用于 value 的所有子类型. 如果您对 GraphQL 感兴趣,我们还有 Blaze-Persistence 实体视图与 graphql-java 的集成:persistence.blazebit.com/documentation/1.5/entity-view/manual/… @ChristianBeikov 谢谢,但我们正在使用 SQPR 从我们的模型/方法以编程方式生成我们的架构 如果您喜欢代码优先的方法,那么您会喜欢 GraphQL 集成。它只处理实际使用的列/表达式,自动减少连接等。【参考方案4】:

在您发表评论后编辑:

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

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

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

实际上,Spring data 和 hibernate 默认无法确定“选定”图形,您需要指定收集选定选项的方式。

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

如你所见,Answer

类的value属性有两个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;
// ..

【讨论】:

问题不是获取Answervalue-关联,而是获取selected 关联,以防valueMCValue。您的回答不包含任何相关信息。 @Stuck 感谢您的回答,能否请您与我分享 MCValue 课程,我会尝试在本地重现您的问题。 您的示例仅有效,因为您将关联 OneToMany 定义为 FetchType.EAGER 但如问题所述:所有关联都是 LAZY @Stuck 自您上次更新以来我更新了我的答案,希望知道我的回答能帮助您解决问题,并帮助您了解加载包括可选关系在内的实体图的方式。 您的“解决方案”仍然受到这个问题所涉及的原始 N+1 问题的困扰:将插入和查找方法放在测试的不同事务中,您会看到 jpa 将向 @ 发出数据库查询987654331@ 为每个答案而不是预先加载它们。

以上是关于JPA 继承 @EntityGraph 包括可选的子类关联的主要内容,如果未能解决你的问题,请参考以下文章

Spring Data JPA:如何使用可选的过滤器参数?

如何使用 BooleanBuilder (QueryDSL) 为可选的 OnetoOne JPA/Hibernate 关系建模谓词?

使用具有复杂条件的 JPA 实体图

具有动态查询的 JPA 也是 Hibernate 中的动态(可选)参数

javascript 当前页面的降价链接的书签(包括可选的选定文本作为报价)

[react] 自定义组件时render是可选的吗?为什么?