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


12

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

在此处输入图片说明

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

使用的ad-hoc @EntityGraphRepository方法我可以确保值是预取的,以防止N + 1上的Answer->Value相关性。虽然我的结果还不错,但是还有一个N + 1问题,因为延迟加载s 的selected关联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

Answers:


8

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实体,并使用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缓存。您能否详细说明依靠高速缓存包含结果的安全性(就一致性而言)?在事务中进行查询时,此功能有效吗?我担心很难发现和零星的懒惰初始化错误。
卡在

1
您需要在同一事务中执行两个查询。只要您这样做并且不清除持久性上下文,它绝对是安全的。您的一级缓存将始终包含MCValue实体。而且您不需要额外的循环。您应该MCValue使用1个查询来获取所有实体,这些查询连接到Answer并使用与当前查询相同的WHERE子句。我在今天的直播中也谈到了这一点:youtu.be/70B9znTmi00?t=238它始于3:58,但我在这之间还提出了一些其他问题……
Thorben Janssen

太好了,谢谢后续!我还要补充一点,该解决方案每个子类需要1个查询。因此,对于我们来说,可维护性还可以,但是这种解决方案可能并不适合所有情况。
卡在

我需要稍微更正我的最后一条评论:当然,每个子类只需要一个受此问题困扰的查询。同样值得注意的是,由于使用,对于子类的属性,这似乎没有问题SINGLE_TABLE_INHERITANCE
卡在

7

我不知道什么弹簧的数据在那里做,但要做到这一点,你通常必须使用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)我们不会致力于使用更复杂的技术堆栈来优化单个查询。(我知道您的lib可以做更多的事情,但是我们更喜欢通用的技术栈,如果没有JPA解决方案,则只会实现自定义查询/转换)。
卡在

1
Blaze-Persistence是开源的,实体视图或多或少在标准的JPQL / HQL之上实现。它实现的功能是稳定的,并且仍然可以在将来的Hibernate版本中使用,因为它可以在标准之上运行。我知道您不想因为单个用例而引入任何东西,但是我怀疑那是可以使用实体视图的唯一用例。引入实体视图通常可以大大减少样板代码的数量,并提高查询性能。如果您不想使用可以帮助您的工具,那就去吧。
Christian Beikov

至少您不了解问题并提供了解决方案。因此,即使答案不能解释原始问题中正在发生的事情以及JPA如何解决该问题,您也将获得赏金。据我了解,JPA只是不支持它,它应该成为功能请求。我将提供另一个赏金,以提供仅针对JPA的更详尽的答案。
卡在

使用JPA根本不可能。您需要TREAT运算符,该运算符在任何JPA提供程序中均不受完全支持,在EntityGraph批注中也不受支持。因此,可以对此建模的唯一方法是通过Hibernate隐式子类型属性解析功能,该功能要求您使用显式联接。
Christian Beikov

1
在您的回答中,视图定义应为interface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Stuck

0

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

我们的数据流如下所示:

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

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

在找到这个之前,我尝试了Spring JPA / Hibernate文档中推荐的投影和所有其他替代方法,但是似乎没有任何方法可以优雅地解决问题,或者至少用大量额外的代码来解决问题


如何解决超级类型未知的加载关联的问题?另外,正如对另一个答案所说的那样,我们想知道是否存在纯JPA解决方案,但是我也认为lib遭受着同样的问题,即selected关联不适用于所有的子类型value
卡在

如果您有兴趣GraphQL,我们也有闪耀的持久性实体意见与graphql的Java的集成:persistence.blazebit.com/documentation/1.5/entity-view/manual/...
基督教Beikov

@ChristianBeikov谢谢,但是我们正在使用SQPR通过模型/方法以编程方式生成架构
aarbor

如果您喜欢代码优先的方法,那么您会喜欢GraphQL集成。它只处理自动获取实际使用的列/表达式,从而减少连接等。
Christian Beikov

0

发表评论后编辑:

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

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

您可以轻松地在此项目中重现并解决您的问题。

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

因此,首先,您必须声明类Answer的NamedEntityGraphs

正如你所看到的,在两个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;
// ..
}

问题不是获取-的关联valueAnswer而是selected在the value是a的情况下获得关联MCValue。您的答案不包含与此有关的任何信息。
卡在

@Stuck感谢您的回答,能否请您与我分享MCValue类,我将尝试在本地复制您的问题。
bdzzaid

您的示例仅起作用是因为您将关联定义OneToMany为, FetchType.EAGER但是如问题中所述:所有关联均为LAZY
卡在

@Stuck我自上次更新以来就更新了我的答案,希望知道我的答案将帮助您解决问题,并帮助您了解加载实体图(包括可选关系)的方式。
bdzzaid

您的“解决方案”仍然受到与该问题有关的原始N + 1问题的困扰:将insert和find方法放在测试的不同事务中,并且您看到jpa会对selected每个答案都发出数据库查询,而不是预先加载它们。
卡在
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.