【问题标题】:JPA @OneToOne select lists with N+1 queriesJPA @OneToOne 选择具有 N+1 个查询的列表
【发布时间】:2018-05-25 01:39:03
【问题描述】:

我实际上是在尝试使用 JPA @OneToOne 注释将 Child 实体链接到其 Parent。

它运行良好,除了在获取Childs 列表时,JPA 引擎(在本例中为 Hibernate)进行 1+n 次查询。

这是 Hibernate 查询的日志:

select child0_.id as id1_0_, child0_.parent as parent3_0_, child0_.value as value2_0_ from child child0_
select parent0_.id as id1_1_0_, parent0_.something as somethin2_1_0_ from parent parent0_ where parent0_.id=?
select parent0_.id as id1_1_0_, parent0_.something as somethin2_1_0_ from parent parent0_ where parent0_.id=?
select parent0_.id as id1_1_0_, parent0_.something as somethin2_1_0_ from parent parent0_ where parent0_.id=?

使用完全相同的实体定义,特别是当我得到一个孩子时,JPA 使用预期的 JOIN 执行查询:

select child0_.id as id1_0_0_, child0_.parent as parent3_0_0_, child0_.value as value2_0_0_, parent1_.id as id1_1_1_, parent1_.something as somethin2_1_1_ from child child0_ left outer join parent parent1_ on child0_.parent=parent1_.id where child0_.id=?

这是Child 实体定义:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "child")
public class Child {

    @Id
    private Long   id;
    @Column
    private String value;
    @OneToOne(optional = false)
    @JoinColumn(name = "parent")
    private Parent parent;

}

还有Parent 实体:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "parent")
public class Parent {

    @Id
    private Long   id;
    @Column
    private String something;

}

您可以在此处找到运行代码的完整示例: https://github.com/Alexandre-Carbenay/demo-jpa-onetoone

在获取带有Parent 的Child 实体列表时,有没有办法避免1+n 查询?

【问题讨论】:

  • 父母是强制性的吗?如果是,则在 OneToOne 注释中设置 optional=false。
  • 我试过添加这个 optional=false,但它并没有改变任何东西

标签: performance hibernate jpa one-to-one select-n-plus-1


【解决方案1】:

我可以重现您的观察结果,但我认为 Hibernate 没有这样做的原因。

避免查询的一种解决方案是使用 JOIN FETCH 之类的

select c from Child c join fetch c.parent

【讨论】:

  • 关键是我不想自己写查询,因为我会在比示例更复杂的情况下使用分页和过滤器但是感谢您的帮助,这可能是我的解决方案'将不得不使用到底
【解决方案2】:

默认情况下,@OneToOne 和 @ManyToOne 关联使用 FetchType.EAGER,这就是您看到 N+1 query issue 的原因。

所以,解决方案相当简单,只需将获取策略设置为 LAZY:

@OneToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "parent")
private Parent parent;

如果你有一个双向@OneToOne 关联,除非你使用字节码增强,否则父端不能懒惰。

【讨论】:

  • 其实,我不希望我的关联被懒惰地加载,而是急切地加载。我真正想要的是 JPA 使用一个数据库查询来获取子项及其父项关联
  • 只使用JOIN FETCH。但是,对于 EAGER 关联,您的所有查询都需要 JOIN FETCH,并且通过 entityManager.find 直接获取将始终触发辅助查询。这就是 EAGER 不好的原因。
  • 我想使用 EAGER,因为在这种情况下,我确定我需要相关的类型信息(用于列表呈现目的的特殊查询),因此在这种情况下使用 EAGER 是有意义的。但我不明白你的意思:你是说,当使用 Hibernate EntityManager 时,如果我结合 EAGER 定义 JOIN FETCH,hibernate 将在初始查询中使用 JOIN,然后对每个关联类型使用辅助查询?但是如果我只使用 JPA 并且不直接与 EntityManager 交互,一切似乎都可以:select ... from child child0_ inner join parent parent1_ on ...
  • EAGER 意味着总是,而不仅仅是在一种情况下,而且您已经有一个查询,因此它可以在地图时使用 LAZY + 在查询时使用 JOIN FETCH。 Hibernate EntityManager 只是 JPA,就像 EclipseLink 或 OpenJPA。如果您使用 JPA 并通过其 id (entityManager.find(Child.class, childId)) 加载 Child 实体,即使您不需要它,Hibernate 也会为此关联发出 JOIN。如果您执行 JPQL 查询并忘记 JOIN FETCH,则会为每个关联实体生成辅助查询,
  • 好的,现在我明白了为什么您建议使用 FETCH JOIN 而不是 EAGER 关联:以确保没有错误/忘记可能暗示二次查询。感谢您的建议
【解决方案3】:

我终于找到了一个比 JOIN FETCH 更好的解决方案,它也适用于 QueryDsl,在存储库方法上使用 @EntityGraph 注释。

这是更新后的Child 定义:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@NamedEntityGraph(name = "Child.withParent", attributeNodes = @NamedAttributeNode("parent"))
@Table(name = "child")
public class Child {
    @Id
    private Long   id;
    @Column
    private String value;
    @OneToOne(optional = false)
    @JoinColumn(name = "parent")
    private Parent parent;
}

还有ChildJpaRepository 定义:

public interface ChildJpaRepository extends JpaRepository<Child, Long>, QueryDslPredicateExecutor<Child> {

    @Override
    @EntityGraph("Child.withParent")
    List<Child> findAll();

    @Override
    @EntityGraph("Child.withParent")
    List<Child> findAll(Predicate predicate);

}

感谢 Simon Martinelli 和 Vlad Mihalcea 的帮助

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-02
    • 2017-01-23
    • 2011-06-23
    • 2020-03-04
    • 2020-07-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多