美文网首页
【Spring JPA总结】JPA 问题汇总:EAGER fet

【Spring JPA总结】JPA 问题汇总:EAGER fet

作者: 伊丽莎白2015 | 来源:发表于2023-01-13 22:28 被阅读0次

参考:


尽管JPA提供了CRUD的抽象操作,使得操作数据库变得十分的方便,但同时又存在另外一些效率等问题需要引起注意。

【本文内容】 JPA问题汇总

问题一:fetch类型=EAGER导致的问题

在JPA中一个entity中想要加载它的相关的entity list时,有两种fetch类型:EAGERLAZY。比如班级和学生是一对多关系,在班级这个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下有两本书: image.png

我们在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=?

相关文章

网友评论

      本文标题:【Spring JPA总结】JPA 问题汇总:EAGER fet

      本文链接:https://www.haomeiwen.com/subject/scmgcdtx.html