Hibernate 投影查询出现 N+1 查询问题

3

使用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);
}

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

Hibernate: select todo0_.id as col_0_0_ from todos todo0_ where todo0_.user_id=? ]

还有N个类似的查询

Hibernate: select todo0_.id as id1_0_0_, todo0_.description as descript2_0_0_, todo0_.target_date as target_d3_0_0_, todo0_.user_id as user_id4_0_0_, user1_.id as id1_1_1_, user1_.password as password2_1_1_, user1_.username as username3_1_1_ from todos todo0_ left outer join users user1_ on todo0_.user_id=user1_.id where 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.
}

更新:通过该查询可能解决问题,但为什么使用接受实体作为参数的构造函数时它不起作用呢?

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 可能会避免这样的问题。 - Karol Dowbecki
无论如何,这与我的问题无关。此外,如果没有被Hibernate映射,它会创建第三个在我这种情况下不需要的表。 - Alexey Usharovski
2个回答

2
这是一个非常普遍的问题,因此我创建了文章消除Spring Hibernate N+1查询详细介绍了解决方案。
使用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);
}

0

我想在这里收集一些有关我的问题的解决方法。有一个简单的解决方案,不需要显式的JPQL查询语句。Spring Data JPA可以将任何带有适当getter和setter的POJO视为投影。

这对我来说非常完美。

public interface ToDoRepository extends CrudRepository<ToDo, Long> {

    List<ToDoRepr> findToDosByUser_Id(Long userId);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接