使用 Hibernate Projection 的预期 N+1 查询

Posted

技术标签:

【中文标题】使用 Hibernate Projection 的预期 N+1 查询【英文标题】:Not expected N+1 queries with Hibernate Projection 【发布时间】:2019-11-03 12:24:44 【问题描述】:

用这样的 Spring Data repository 面对 N+1 查询问题

public interface ToDoRepository extends CrudRepository<ToDo, Long> 

    @Query("select new com.package.repr.ToDoRepr(t) from ToDo t " +
            "where t.user.id = :userId")
    List<ToDoRepr> findToDosByUserId(@Param("userId") Long userId);

我在日志中看到一个这样的查询

休眠: 选择 todo0_.id 为 col_0_0_ 从 待办事项 todo0_ 在哪里 todo0_.user_id=? ]

还有N个这样的查询

休眠: 选择 todo0_.id 为 id1_0_0_, todo0_.description 作为descript2_0_0_, todo0_.target_date 作为 target_d3_0_0_, todo0_.user_id 作为 user_id4_0_0_, user1_.id 为 id1_1_1_, user1_.password 作为密码2_1_1_, user1_.username 作为 username3_1_1_ 从 待办事项 todo0_ 左外连接 用户 user1_ 在 todo0_.user_id=user1_.id 在哪里 todo0_.id=?

ToDoRepr 是一个简单的 POJO。使用接受 ToDo 实体作为参数的构造函数。

这是我在此查询中使用的两个 JPA 实体

@Entity
@Table(name = "todos")
public class ToDo 

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String description;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @Column
    private LocalDate targetDate;

    // geters, setters, etc.

@Entity
@Table(name = "users")
public class User 

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @OneToMany(
            mappedBy = "user",
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private List<ToDo> todos;

    // geters, setters, etc.

UPD。可以通过该查询解决问题,但为什么它不能与接受实体作为参数的构造函数一起使用?

public interface ToDoRepository extends CrudRepository<ToDo, Long> 

    @Query("select new com.package.repr.ToDoRepr(t.id, t.description, t.user.username, t.targetDate) " +
            "from ToDo t " +
            "where t.user.id = :userId")
    List<ToDoRepr> findToDosByUserId(@Param("userId") Long userId);

【问题讨论】:

你有@OneToMany(mappedBy = "user" ...),这使得关系是双向的。您是否需要在User 类中映射此关系?从User 中删除todos 可能会避免这样的问题。 无论如何,这与我的问题无关。也没有通过 Hibernate 映射创建我的情况不需要的第三个表。 【参考方案1】:

这是一个非常常见的问题,因此我创建了文章 Eliminate Spring Hibernate N+1 queries 详细介绍了解决方案

使用 Hibernate 的最佳做法是将所有关联定义为 Lazy,以避免在不需要时获取它。 更多原因,请查看 Vlad Mihalcea 的文章https://vladmihalcea.com/eager-fetching-is-a-code-smell/

为了解决您的问题,在您的 ToDo 类中,您应该将 ManyToOne 定义为 Lazy

@Entity
@Table(name = "todos")
public class ToDo 

    ...

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    ...

    // geters, setters, etc.

如果您需要访问 ToDoRepr 中的用户,默认情况下不会加载,因此您需要将其添加到查询中:

JPQL,使用 JOIN FETCH
public interface ToDoRepository extends CrudRepository<ToDo, Long> 

    @Query("select new com.package.repr.ToDoRepr(t) " +
            "from ToDo t " +
            "inner join fetch t.user " +
            "where t.user.id = :userId")
    List<ToDoRepr> findToDosByUserId(@Param("userId") Long userId);

JPA,使用 EntityGraph
public interface ToDoRepository extends CrudRepository<ToDo, Long> 

    @EntityGraph(attributePaths = "user")
    List<ToDoRepr> findToDosByUser_Id(Long userId);

【讨论】:

【参考方案2】:

我想在这里收集一些关于我自己的问题的解决方法。有一个没有显式 JPQL 查询的简单解决方案。 Spring Data JPA 可以将任何具有适当 getter 和 setter 的 POJO 视为投影。

正好适合我

public interface ToDoRepository extends CrudRepository<ToDo, Long> 

    List<ToDoRepr> findToDosByUser_Id(Long userId);

【讨论】:

以上是关于使用 Hibernate Projection 的预期 N+1 查询的主要内容,如果未能解决你的问题,请参考以下文章

使用 Hibernate Projection 的预期 N+1 查询

使用 D3 的 projection.stream() 的正确方法是啥?

ClickHouse 使用物化字段投影 PROJECTION 提升性能

Clickhouse Projection 特性探索

ClickHouse SQL 极简教程使用物化字段投影 PROJECTION 提升性能

CAF(C++ actor framework)使用随笔(projection 用法)