JPA编程陷阱-如何避免N+1问题

在系统不是特别复杂的前提下,合理的使用Spring JPA确实是挺方便的。尤其是使用repository接口,对于一些简单的sql,只需要把method名字写好,在启动程序的时候Spring就会帮你做check,测试都能省掉。
但是,Spring JPA方便归方便,如果你不能准确的把握其中的细节,系统到后面数据躲起来,性能上是要大打折扣的。所以,只有准确的把握Spring JPA的编程陷阱,并且熟知他们的解决方案,才能更好的发挥Spring JPA的功效。

之前也总结过几篇JPA的编程陷阱。这次我想说一下如何N+1问题。

何为N+1问题?简单的说,就是我们想通过查询取N条数据的时候,这里面存在表间关系,理想情况下我们只需要通过JOIN就能通过1次SQL文的查询实现。但是,如果你把这个事情交给JPA去做的话,在某些情况下,需要执行N+1次SQL的查询才能取到结果。

例:

假设有这样的两个表

@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int? = null,

    val name: String,

    @ManyToOne
    val userCategory: UserCategory
)

@Entity
@Table(name = "user_categories")
data class UserCategory(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int? = null,

    val name: String,

    @OneToMany(mappedBy= "userCategory")
    val users: List<User>
)

然后,有这么两个UserRepository。

@Repository
interface UserRepository : JpaRepository<User, Int> {
    fun findByNameLike(name: String): List<User>
}

@Repository
interface UserCategoryRepository : JpaRepository<UserCategory, Int> {
    fun findByName(name: String): UserCategory?
}

首先UserRepository,这种情况下JPA其实是是会帮你去做inner join把user_category的信息也取出来的。
但是,UserCategoryRepository的findByName的话,因为是被动的关联关系,JPA是不会inner join去取的。这种情况就会发生N+1。

除去上面这种情况,还有就是下面这样的例子:

entityManager.createQuery("select u from User u where 1=1 $condition", User::class.java)

我自己写了一段JPQL的语句,想通过自定义的查询语句去查询user表。那么,上面这种写法也肯定会发生N+1问题。

具体N+1问题怎么去解决。JPA提供了一个方案就是使用FETCH JOIN。
前面那个JPARepository的接口需要自己写SQL语句。后面entityManager的例子我们实际来看一下:

entityManager.createQuery("select u from User u INNER JOIN FETCH u.userCategory where 1=1 $condition", User::class.java)

写法其实很简单,但是注意这里写的是JOIN FETCH,跟native SQL的JOIN是不一样的。具体关于JPQL的写法,可以查看官方文档。

以上就是对JPA N+1问题的解读,希望以后可以更高效的使用JPA。

https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/fetch-join.html

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Close Bitnami banner
Bitnami