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

2023-01-14  本文已影响0人  伊丽莎白2015

参考:


【本文内容】 Spring Boot JPA删除

1. 在Repository中进行删除

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

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 = ?

上一篇下一篇

猜你喜欢

热点阅读