参考:
尽管JPA提供了CRUD的抽象操作,使得操作数据库变得十分的方便,但同时又存在另外一些效率等问题需要引起注意。
【本文内容】问题一:fetch类型=EAGER导致的问题
在JPA中一个entity中想要加载它的相关的entity list时,有两种fetch类型:EAGER
和LAZY
。比如班级和学生是一对多关系,在班级这个entity中,配置了学生(关系为一对方),那么加载学生这个list的时候,就用到了fetch类型。
-
EAGER
类型:和父entity一起获取子entity list(一般用到了join语句)。这也导致JPA可能返回不必要的数据,从而影响效率。 -
LAZY
类型:按需获取子entity list,并不会和父entity一起返回。LAZY
类型有可能会抛出LazyInitializationException
异常。
【例子】
数据原型,查看:https://www.jianshu.com/p/1c279b221527
书店里有很多书,所以书店和书之间,是一对多关系:
@Entity
@Table(name = "book_store")
public class BookStore {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "book_store_id")
private Set<Book> books = new HashSet<>();
}
public interface BookStoreRepository extends JpaRepository<BookStore, Integer> {
List<BookStore> findByNameContaining(String name);
}
方法findByNameContaining(),按name模糊查询,如果fetch类型为EAGER
,那么在返回数据的时候,会同时查询book表。sql如下:
第一次会按name like查询book_store表:
select
bookstore0_.id as id1_2_,
bookstore0_.name as name2_2_
from
book_store bookstore0_
where
bookstore0_.name like ? escape ?
如果fetch是LAZY
的话,就不需要以下的查询了。
而EAGER
的话,会再次按book_store_id进行查询,逐次返回各个id下的book list数据,如果上面的按name模糊查询返回3个bookStore(比如id=1, 2, 3),那么下面的sql语句会执行三次,传入的id分别为1, 2, 3:
select
books0_.book_store_id as book_sto3_1_0_,
books0_.id as id1_1_0_,
books0_.id as id1_1_1_,
books0_.name as name2_1_1_
from
book books0_
where
books0_.book_store_id=?
【总结】fetch类型=EAGER
时,会查询不必要的数据,也会导致N+1
的问题。
【解决方式 1-1】想要解决上述问题,可以使用fetch类型=LAZY
。
另外,在JPA注解的x对一关联(如@ManyToOne
, @OneToOne
)中fetch默认类型都是EAGER
,如果想用LAZY
,需要显示指定出来,如@ManyToOne(fetch = FetchType.LAZY)
。
问题二:fetch类型LAZY导致LazyInitializationException异常
@Entity
@Table(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "book_store_id")
private BookStore bookStore;
}
如果fetch类型为LAZY
,如果在transactional context之外(即没有事务),那么就会报:LazyInitializationException
异常。
原因是会先查多方的数据(from book),但是再获取一方(bookStore)的时候session已经关闭了,因此报错。
我们尝试按book名字模糊查询,并转化成BookWithBookStoreView
类:
@Test
public void findByNameContainingTest() {
List<BookWithBookStoreView> bookList = findByNameContaining("book");
System.out.println(bookList);
}
private List<BookWithBookStoreView> findByNameContaining(String name) {
List<Book> bookList = bookRepository.findByNameContaining(name);
return bookList.stream()
.map(book -> new BookWithBookStoreView(book.getId(), book.getName(), book.getBookStore()))
.collect(Collectors.toList());
}
抛错:org.hibernate.LazyInitializationException: could not initialize proxy [com.entity.BookStore#1] - no Session
想要避免LazyInitializationException
异常,可以尝试:
- 使用fetch类型为
EAGER
,但这又回到了问题一,即导致N+1问题。 - 使用
@Transactional
来标记上述的测试方法queryTest(),使得这个方法在事务的上下文中执行,这样就不会导致session被关闭了。但同样的,这会致致N+1的问题。 - 修改Hibernate的初始化参数,使得避免产生上述异常。但同样的,这个解决方式会带来另外一些问题,如它会执行额外的SQL等。
【解决思路】尝试从repository方面入手来解决JPA在执行完1个SQL后,再获取相关联的数据时不要抛LazyInitializationException
异常。
【解决方式 2-1】查询结果自定义DTO对象
Spring Data can help retrieve partial view of a JPA @Entity with interface-based or class-based projection (DTO classes)
public interface BookDTO {
Integer getId();
String getName();
BookStoreDTO getBookStore();
}
public interface BookStoreDTO {
Integer getId();
String getName();
}
在repository层,用BookDTO
代替原来的Book返回:
public interface BookRepository extends JpaRepository<Book, Integer> {
List<BookDTO> findByNameContaining(String name);
}
测试:
@Test
public void findByNameContainingTest() {
List<BookWithBookStoreView> bookList = findByNameContaining("book");
System.out.println(bookList);
}
private List<BookWithBookStoreView> findByNameContaining(String name) {
List<BookDTO> bookList = bookRepository.findByNameContaining(name);
return bookList.stream()
.map(book -> new BookWithBookStoreView(book.getId(), book.getName(),
new BookStore(book.getBookStore().getId(), book.getBookStore().getName())))
.collect(Collectors.toList());
}
sql,可以看到避免了N+1的问题,在where name like的时候,同时也用inner join获取了bookStore的数据:
select
book0_.id as col_0_0_,
book0_.name as col_1_0_,
book0_.book_store_id as col_2_0_,
bookstore1_.id as id1_2_,
bookstore1_.name as name2_2_
from
book book0_
inner join book_store bookstore1_ on book0_.book_store_id=bookstore1_.id
where
book0_.name like ? escape ?
【解决方式 2-2】使用@EntityGraph
使用@EntityGraph
注解,标注在repository的方法上,用来声明这是一个查询配置的属性的query。
声明我们需要查询bookStore:
public interface BookRepository extends JpaRepository<Book, Integer> {
@EntityGraph(attributePaths = "bookStore")
List<Book> findByNameContaining(String name);
}
这时候我们用第#2章一开始的test去执行,发现不会再报LazyInitializationException
异常。
sql和解决方式-1 一样,在where name like的基础上,会再用left outer join book_store表,这样就极好的避免了N+1的问题,同时也避免了LazyInitializationException
异常:
select
book0_.id as id1_1_0_,
bookstore1_.id as id1_2_1_,
book0_.book_store_id as book_sto3_1_0_,
book0_.name as name2_1_0_,
bookstore1_.name as name2_2_1_
from
book book0_
left outer join book_store bookstore1_ on book0_.book_store_id=bookstore1_.id
where
book0_.name like ? escape ?
【解决方式 2-3】使用JPQL JOIN FETCH
JPQL(Java Persistence Query Language)支持以JOIN的方式在一个query中关联相关的数据并返回。
public interface BookRepository extends JpaRepository<Book, Integer> {
@Query(value="FROM Book b LEFT JOIN FETCH b.bookStore where b.name like %:name%")
List<Book> findByNameContaining(String name);
}
sql语句如下,可以看到也是用了left outer join来获取数据,同时也避免了N+1的问题:
select
book0_.id as id1_1_0_,
bookstore1_.id as id1_2_1_,
book0_.book_store_id as book_sto3_1_0_,
book0_.name as name2_1_0_,
bookstore1_.name as name2_2_1_
from
book book0_
left outer join book_store bookstore1_ on book0_.book_store_id=bookstore1_.id
where
book0_.name like ?
问题三:执行N+1次的问题:查询方面
当JPA想要获取实体内的子entity list的数据时,不得不执行多余的N次SQL,往往发生在以下情形中:
- 当fetch=
EAGER
时,当获到取bookStore的数据后,会再逐个按bookStoreId获取下面的book list数据。(在#1中有详细介绍)。 - 当fetch=
LAZY
时,上述#2一开始的测试代码,如果加上@Transactional,同样会有N+1的问题。它会先查询book表,where name like ?,查询出book list后,再按book.book_store_id的值,逐个本询book_store表。 - 不仅仅是查询,delete的时候也会有这个问题(留到下章讲)。
【解决方式 3-1】使用@EntityGraph
在上述第#2章的【解决方式 2-2】有介绍。
【解决方式 3-2】使用JPQL JOIN FETCH
在上述第#2章的【解决方式 2-3】有介绍。
问题四:执行N+1次的问题:删除方面
假设bookStore id = 1下有两本书:我们在BookRepository中希望按bookStoreId进行删除:
public interface BookRepository extends JpaRepository<Book, Integer> {
@Transactional
void deleteByBookStoreId(int bookStoreId);
}
测试:
@Test
public void deleteByBookStoreIdTest() {
bookRepository.deleteByBookStoreId(1);
}
相应的sql,首先是查询出bookStoreId下所有的book list:
select
book0_.id as id1_1_,
book0_.book_store_id as book_sto3_1_,
book0_.name as name2_1_
from
book book0_
inner join
book_store bookstore1_
on book0_.book_store_id=bookstore1_.id
where
bookstore1_.id=?
然后进行逐个删除,因为数据库中相应的数据有2条(id = 1, 2),所以这里执行了两次:
delete from book where id=?
delete from book where id=?
即,在删除的时候,我们发现JPA会逐一删除,这样会导致N+1的问题。
【解决方式 4-1】: 定义DELETE语句
在repository层,我们自己定义DELETE语句来按bookStoreId进行删除:
public interface BookRepository extends JpaRepository<Book, Integer> {
@Modifying
@Transactional
@Query("DELETE FROM Book b WHERE b.bookStore.id = :bookStoreId")
void deleteInBulkByBookStoreId(int bookStoreId);
}
这样在执行的时候,可以有效的避免按book.id进行逐个删除,sql如下(只有一个):
delete from book where book_store_id=?