【问题标题】:Too many queries problem with JPA + Hibernate even when using @Fetch(FetchMode.JOIN)即使使用 @Fetch(FetchMode.JOIN),JPA + Hibernate 也会出现太多查询问题
【发布时间】:2019-12-30 09:48:28
【问题描述】:

我正在使用 Spring Boot 开发 REST 应用程序,并且正在尝试优化查询的性能。我目前正在使用导致性能问题的存储库中的findAll。代码如下:

人物实体

@Entity
@Table(name = "cd_person")
@Data
@NoArgsConstructor
public class Person {
    ....
    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "password_id")
    @Fetch(FetchMode.JOIN)
    private Password password;
    ....
    @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name = "cd_person_role",
        joinColumns = @JoinColumn(name = "person_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    @Fetch(FetchMode.JOIN)
    private Set<Role> roles = new HashSet<>();
}

密码实体

@Entity
@Table(name = "cd_password")
@Data
@NoArgsConstructor
public class Password {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;

    @Column(name = "password_hash", nullable = false)
    private String passwordHash;
    .......
}

角色实体

@Entity
@Table(name = "cd_role")
@Data
@NoArgsConstructor
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "role_type")
    @Enumerated(EnumType.STRING)
    private RoleType roleType;
    ....
}

个人资料库

public interface PersonRepository extends CrudRepository<Person, Long> {

    Optional<Person> findByEmail(String email);

}

当我执行personRepository.findAll() 时,会针对人员表中的每一行触发选择查询,以便在我访问人员时获取密码和角色。我知道我可以在存储库中使用@Query 注释和JOIN FETCH 来强制生成单个查询,但我想知道是否还有其他方法可以这样做。我正在寻找我们可以在实体级别做的事情以减少查询。

使用spring boot 2.1.5-RELEASE版本及相关依赖。

附言。 @Data@NoArgsConstructor 是 Lombok 注释。

【问题讨论】:

    标签: java hibernate spring-boot spring-data-jpa


    【解决方案1】:

    这应该可行:

    public interface PersonRepository extends CrudRepository<Person, Long> {
         @Override
            @Query("SELECT p FROM Person p JOIN FETCH p.roles JOIN FETCH p.password ")
            Iterable<Person> findAll();
    }
    

    【讨论】:

      【解决方案2】:

      您应该将 @BatchSize 放在 Password 类的顶部

      @Entity
      @Table(name = "cd_password")
      @Data
      @NoArgsConstructor
      @BatchSize(size = 50)
      public class Password {
      ...
      }
      

      以下是带有@BatchSize 的查询:

      Hibernate: 
          select
              person0_.id as id1_1_,
              person0_.password_id as password2_1_ 
          from
              cd_person person0_
      Hibernate: 
          select
              password0_.id as id1_0_0_,
              password0_.password_hash as password2_0_0_ 
          from
              cd_password password0_ 
          where
              password0_.id in (
                  ?, ?, ?, ?, ?
              )
      

      【讨论】:

      • 感谢您的努力,但我想进一步优化它。批处理会更好,但仍然不是最佳的。如果您可以提供任何关于批处理性能如何优于连接的参考,我会这样做并接受答案。
      【解决方案3】:

      我的建议是:

      1. 尝试重构并使用延迟获取。
      2. 我可能不太了解这部分,但你为什么需要personRepository.findAll()?我认为您只需要personRepository.findById() 之类的东西,这样您就可以轻松获取角色和其他数据。在这里选择所有人似乎是一个巨大的负担。
      3. 您以后可能需要JpaRepository 的扩展功能,因此现在可能值得更改它,而不是稍后再工作。

      【讨论】:

        【解决方案4】:

        最小的代码更改是使用 spring data 中的 ad-hoc EntityGraph 功能。只需覆盖 PersonRepositoryfindAll() 并使用 @EntityGraph 来配置图形。此图中的所有实体将一起获取。

        public interface PersonRepository extends CrudRepository<Person, Long> {
        
            @EntityGraph(attributePaths = { "password", "roles" })
            public List<Person> findAll();
        
        }
        

        在幕后它的工作方式类似于JOIN FETCH。只会生成带有 LEFT JOIN 的单个 SQL。

        【讨论】:

        • 与 JOIN FETCH 相比有什么性能优势/劣势,还是代码行数更少?
        • 如果您将遍历所有检索到的人员并访问其密码和角色,则性能良好,因为所有这些都已通过单个 SQL select 获取,没有 N+1 问题??
        • 我在问这个与 JOIN FETCH 相比。使用@Query 好还是定义实体图好?
        • 它们是一样的。它生成与 JOIN FETCH 相同的 SQL
        【解决方案5】:

        对您的问题的不满意答案是:不,无法注释/配置实体,以便获取模式也适用于查询。

        您正确地找到了自己,您可以manipulate the query itself。对此的替代方法是使用 Hibernate's fetch profiles 或利用 JPA entity graphs - 但它们都需要在查询/会话级别进行编程干预。

        【讨论】:

          【解决方案6】:

          我将保留实体并使用@Query 注释覆盖存储库中的findAll 方法。 这样,代码重构最少(仅更改一个存储库而不是更改实体)。

          【讨论】:

            【解决方案7】:

            您不能使用延迟提取并删除 @Fetch 吗?在您的实体之上使用 @NamedQuery 并使用休眠会话在自定义服务中调用 session.createNamedQuery 即可。

            如果您负担得起不使用默认的personRepository.findAll() 但此自定义服务您将运行优化查询。我知道它并不能完全回答您的问题,但我和我的团队遇到了完全相同的问题,我们就是这样做的。

            【讨论】:

            • 我不更改存储库逻辑的唯一原因是我必须进行代码重构,这太费力了。如果我没有其他选择,我可能不得不这样做。
            猜你喜欢
            • 2014-03-18
            • 2012-04-15
            • 1970-01-01
            • 1970-01-01
            • 2015-03-12
            • 2014-03-27
            • 1970-01-01
            • 2021-01-21
            • 1970-01-01
            相关资源
            最近更新 更多