美文网首页
【Spring JPA总结】Spring Boot JPA删除数

【Spring JPA总结】Spring Boot JPA删除数

作者: 伊丽莎白2015 | 来源:发表于2023-01-14 23:16 被阅读0次

    参考:


    【本文内容】 Spring Boot JPA删除

    1. 在Repository中进行删除

    Spring Data Repository提供了两个方法进行数据的删除:

    • delete(entity)
    • deleteById(entityId)

    2. 使用CascadeType.ALLCascadeType.REMOVE

    上述的delete方法,100%适用于如果entity中没有关联的时候。但如果entity中存在某些JPA的关联(在数据库中有外键存在),并且当我们没有添加CascadeType.ALLCascadeType.REMOVE时,会报错。

    书店(bookStore)中有很多书(book),属于一对多关系。例子数据模式参考: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(mappedBy = "bookStore")
        private Set<Book> books = new HashSet<>();
    }
    
    @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;
    }
    

    在BookStoreRepository中想要删除,遇到错误:

        @Test
        public void deleteTest() {
            bookStoreRepository.deleteById(1);
        }
    

    报错,原因是bookStore如果删除了,但是它的id作为book.book_store_id的外键存在于book表中,所以遇到了数据库级别的错误:

    2023-01-15 15:47:02.777 WARN 83246 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1451, SQLState: 23000
    2023-01-15 15:47:02.777 ERROR 83246 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Cannot delete or update a parent row: a foreign key constraint fails...

    想要fix这个问题,可以在@OneToMany上加CascadeType.ALLCascadeType.REMOVE,这样在删除bookStore.id=1的数据时,也会同时删除book表中所有bookStoreId=1的数据:

        @OneToMany(mappedBy = "bookStore", cascade = CascadeType.ALL)
        private Set<Book> books = new HashSet<>();
    

    cascade在英语单词中的意思是串联、级联。CascadeType有很多值,如ALLPERSISTMERGEREMOVEREFRESH以及2.0后加进来的DETACH
    其中ALL包含了另外5个值,即cascade=ALL等价于cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}

    CascadeType.REMOVE表示当做删除操作的时候,需要自动删除它下面关联的entity list。在我们的例子中,即在删除bookStore的时候,如果配置了CascadeType=ALL/REMOVE,则会自动删除它下面通过@OneToMany关联的book的数据。

    上述的测试,除了查询的sql外,删除相关的sql如下,因为相关的book有两条,所以有三个删除语句,可以看到使用CascadeType.ALLCascadeType.REMOVE来删除数据会导致N+1的问题

    delete from book where id=?
    delete from book where id=?
    delete from book_store where id=?

    3. 使用orphanRemoval

    CascadeType.REMOVE vs orphanRemoval:

    CascadeType.REMOVE强调的是当父entity删除的时候,连带着删除子entity。发生在父entity 删除的时候。

    orphanRemoval = true,是JPA 2.0引入的,主要是提供了一种可以删除orphan entity的方式。强调的是当子entity脱离了父entity的关联的时候,则删除子entity。这里的脱离父entity,可以是父entity在save的时候发现子entity的list改变了(比如移掉了一个子entity),这时候这个子entity就会被删除。发现了父entity 保存的时候。

    @Entity
    @Table(name = "book_store")
    public class BookStore {
        ...
    
        @OneToMany(mappedBy = "bookStore", cascade = CascadeType.PERSIST, orphanRemoval = true)
        private Set<Book> books = new HashSet<>();
    }
    

    在数据库中的数据:bookStoreId = 1, 有2本书:
    [Book{id=2, name='book-2'}, Book{id=1, name='book-1'}]

    测试:

    @Test
        public void orphanRemovalTest() {
            BookStore bookStore = bookStoreRepository.findById(1).get();
            bookStore.setBooks(new HashSet<>());
            bookStoreRepository.save(bookStore);
        }
    

    sql:

    select
    bookstore0_.id as id1_2_0_,
    bookstore0_.name as name2_2_0_
    from
    book_store bookstore0_
    where
    bookstore0_.id=?

    以下是save产生的sql,首先会先查询一次:

    select
    bookstore0_.id as id1_2_0_,
    bookstore0_.name as name2_2_0_
    from
    book_store bookstore0_
    where
    bookstore0_.id=?

    如果有setBooks的调用,所以会查询book表:

    select
    books0_.book_store_id as book_sto3_1_0_,
    books0_.id as id1_1_0_,
    books0_.id as id1_1_1_,
    books0_.book_store_id as book_sto3_1_1_,
    books0_.name as name2_1_1_
    from
    book books0_
    where
    books0_.book_store_id=?

    因为我们把books给清空了,那么原先两个books(id=1,2)就变成了orphan entity了,因为我们配置了orphanRemoval=true,所以会自动清除这两个orphan books:

    delete from book where id=?
    delete from book where id=?

    注:orphanRemoval=true,需要在CascadeType.PERSIST或是CascadeType.ALL下使用(因为CascadeType.ALL包含了所有的的级联)。

    4. 使用JPQL来批量删除数据

    在#2中的sql可以看到,使用CascadeType.ALL或CascadeType.REMOVE来删除数据会导致N+1的问题。

    也就是说JPA在删除的时候,是逐条删除的。在数据量多的情况下这样比较影响效率,理想状态下,我们希望使用一条sql来删除相关联的数据(比如:delete from book where book_store_id = ?)。

    这时候可以用JPQL来删除数据,如果repository中不用@Transactional进行标记,可以在service调用的时候,在事务context中进行调用:

    public interface BookStoreRepository extends JpaRepository<BookStore, Integer> {
        @Modifying
        @Transactional
        @Query("DELETE FROM BookStore b WHERE b.id = :id")
        void deleteByIdWithJPQL(int id);
    }
    
    public interface BookRepository extends JpaRepository<Book, Integer> {
        @Modifying
        @Transactional
        @Query("DELETE FROM Book b WHERE b.bookStore.id = :bookStoreId")
        void deleteInBulkByBookStoreId(int bookStoreId);
    }
    

    测试:

        @Test
        public void deleteWithJPQLTest() {
            bookRepository.deleteInBulkByBookStoreId(1);
            bookStoreRepository.deleteByIdWithJPQL(1);
        }
    

    相关sql:

    delete from book where book_store_id=?
    delete from book_store where id=?

    需要先删除子entity(即book),如果先删除bookStore,那么因为外键还留在book表中,导致报错。

    5. 逻辑删除

    很多时候,我们的系统除了直接删除数据外,一般会选择一列来记录数据的状态,比如DELETED或是STATUS。另外可能还需要两列来记录创建时间以及最近一次的修改时间,即:CREATED_TIME,UPDATED_TIME。

    @Entity
    @Table(name = "book_store")
    public class BookStore {
        ...
    
        @OneToMany(mappedBy = "bookStore", cascade = CascadeType.ALL)
        private Set<Book> books = new HashSet<>();
    
        private boolean deleted;
    
        @CreationTimestamp
        private LocalDateTime createdTime;
    
        @UpdateTimestamp
        private LocalDateTime updatedTime;
    
    }
    
    @Entity
    @Table(name = "book")
    public class Book {
        ...
    
        @ManyToOne
        @JoinColumn(name = "book_store_id")
        private BookStore bookStore;
    
        private boolean deleted;
    
        @CreationTimestamp
        private LocalDateTime createdTime;
    
        @UpdateTimestamp
        private LocalDateTime updatedTime;
    }
    

    在BookStoreServiceImpl类是加入方法:

    @Service
    public class BookStoreServiceImpl implements BookStoreService {
        @Transactional
        public void softDeleteManually(int id) {
            BookStore bookStore = bookStoreRepository.findById(id).get();
            bookStore.setDeleted(true);
    
            bookStore.getBooks().forEach(book -> book.setDeleted(true));
    
            bookStoreRepository.save(bookStore);
        }
    }
    

    测试:

        @Test
        public void test() {
            bookStoreService.softDeleteManually(1);
        }
    

    bookStore id=1,在数据库中有2条book数据,除了select相关的sql,会产生3条update的操作:

    update book_store
    set
    created_time=?,
    deleted=?,
    name=?,
    updated_time=?
    where id=?

    update book
    set
    book_store_id=?,
    created_time=?,
    deleted=?,
    name=?,
    updated_time=?
    where id=?

    update book
    set // 同上,略
    where id=?

    @CreationTimestamp@UpdateTimestamp从Hibernate 4.3版本后被加入。在查询的时候,我们需要按deleted=false的条件取数据:

    public interface BookStoreRepository extends JpaRepository<BookStore, Integer> {
        List<BookStore> findByNameContainingAndDeletedFalse(String name);
    }
    

    6. @Where@SQLDelete逻辑删除

    @Where@SQLDelete可以被定义在entity类上,从而进行逻辑删除。

    可以使用@Where(clause = ...)来排除所有已经被“删除”的数据。使用@SQLDelete(sql = ...)来定义删除的时候需要执行的语句。

    具体来看:

    @SQLDelete(sql = "UPDATE book_store SET deleted = 1 WHERE id = ?")
    @Where(clause = "deleted = 0")
    @Entity
    @Table(name = "book_store")
    public class BookStore {
        ...
    }
    
    @SQLDelete(sql = "UPDATE book SET deleted = 1 WHERE id = ?")
    @Where(clause = "deleted = 0")
    @Entity
    @Table(name = "book")
    public class Book {
        ...
    }
    

    测试:

        @Test
        public void deleteTest() {
            bookStoreRepository.deleteById(1);
        }
    

    相关的sql:
    先查询bookStore的数据,按id和deleted=0查询:

    select // 略
    from book_store bookstore0_
    where
    bookstore0_.id=?
    and (
    bookstore0_.deleted = 0
    )

    再查询book的数据,也会自动加上deleted=0的条件:

    select
    books0_.book_store_id as book_sto6_1_0_,
    books0_.id as id1_1_0_,
    books0_.id as id1_1_1_,
    books0_.book_store_id as book_sto6_1_1_,
    books0_.created_time as created_2_1_1_,
    books0_.deleted as deleted3_1_1_,
    books0_.name as name4_1_1_,
    books0_.updated_time as updated_5_1_1_
    from book books0_
    where
    (
    books0_.deleted = 0
    )
    and books0_.book_store_id=?

    开始做“删除”操作,其实是逻辑删除,所以是update语句,因为bookStore下有两个book数据,所以会有3条update语句(逐个更新):

    UPDATE book SET deleted = 1 WHERE id = ?
    UPDATE book SET deleted = 1 WHERE id = ?
    UPDATE book_store SET deleted = 1 WHERE id = ?

    相关文章

      网友评论

          本文标题:【Spring JPA总结】Spring Boot JPA删除数

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