Spring Data JPA OneToMany级联,多方删除修改新增详解

前言

近期的项目中使用Spring Data JPA。JPA带来很大的便捷,但它内部映射关系及持久化机制如果理解不到位会出现很多问题。不同的配置将会产生不同的执行过程。如果不了解其运行机制,很容易在一个问题上摸索很久,找不到答案。近期碰到一个问题,在一对多关系中,先进行了一方的查询,然后找到需要删除多方数据,做删除操作。看似简单的删除,但JPA在不同的onToMany配置下,却呈现出不同的执行结果。正好借此机会做了oneToMany不同配置的实验,在此做个记录。也希望通过实验,找到不同场景下最佳的配置方式。

进入正文前,我先啰嗦一下级联操作。一方在oneToMany上设置的级联保存和更新很好理解,多方会随着一方进行保存和更新。但是级联删除其实只是指一方删除时会把关联的多方数据全部删除,并不能删除一方维护的多方list中remove掉的数据。所以本文所讨论的实验和是否设置级联删除是没有关系的。

本文基于实验,我们先设定有如下对象,User为一方,ContactInfo为多方。每个user有多个contactInfo。

所做的操作是先查询User,然后对关联的ContactInfo做增删改。

public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String userName;

    private String password;

    @Fetch(FetchMode.SUBSELECT)
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private List contactInfos = new ArrayList<>();
}

public class ContactInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String phoneNumber;

    private String address;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    @JsonIgnore
    private User user;
}

一对多关系,通过@onToMany注解实现,此注解有个属性mappedBy,这个属性默认为空(上面示例代码未设置,取默认值),代表一方要维护关系。如果mappedBy设置为一方对象的值,如mappedBy = "user",代表一方放弃维护关系,具体表现就是在插入或者删除操作的时候,一方不会去update多方的外键。这在后面的实验中会有所体现。

在讲解实验前,为了照顾没时间看完全文的读者,我先给出最终的结论:一方应放弃维护关系,由多方自行维护。这适用于绝大多数的场景。下文会详细描述整个实验过程以及如何得出的结论。

我们先看上面示例代码这种配置(不设置mappedBy),也就是一方不放弃维护关系的实验。

一方不放弃维护关系

关系配置代码

User类
@Fetch(FetchMode.SUBSELECT)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private List contactInfos = new ArrayList<>();

ContactInfo类
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@JsonIgnore
private User user;

实验如下

1、多方新增

 持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().add(ContactInfo.builder()
        	.address("朝阳望京街道")
        	.phoneNumber("18612938250").build());
userRepository.save(user);

JPA执行过程:

1、先插入一条userId为空的contactInfo(由于未设置user)

 insert into contact_info (address, phone_number, user_id) values (?, ?, ?)

2、然后更新userId

update contact_info set user_id=? where id=?

分析:

步骤1的insert操作是一方级联persist触发的操作。步骤2是因为一方还要维护外键,所以会对多方新增的数据update外键。

问题:

如果数据库设置了外键不能为空,那么步骤1无法执行。为了避免这个问题,可以在构造ContactInfo的时候把user对象设置进来。

2、多方更新:

持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().get(0).setPhoneNumber("88888888");
userRepository.save(user);

JPA执行过程:

1、直接根据多方主键进行更新

update contact_info set address=?, phone_number=?, user_id=? where id=?

 

分析:

因为设置了级联update,所以save user的时候会update多方contactInfo

 

3、多方删除:

A)仅从一方的list中remove

持久化代码:

User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
user.getContactInfos().remove(deletedContact);
userRepository.save(user);

JPA执行过程:

只是把deletedContact的user_Id更新为null,相当于断开了关系连接。如果您的表设计外键不能为空,则数据库报错。

update contact_info set user_id=null where user_id=? and id=?

分析:

所以从list中移除deletedContact,意味着user和此条contactInfo的关系断开了。又因为一方没有放弃关系的维护,这个操作会触发被remove掉的deletedContact的外键userId被置空。

此时去掉userRepository.save(user),什么都不会发生。这好像是废话,不过结合下面的实验对比来看,是有不同效果的。

问题:

并没有删除掉deletedContact数据,只是外键被置空。如果一方和多方是聚合关系,并且不想真正删除多方数据(多方数据可以和别的一方数据再次关联),那么适用这种方式。但如果是组合关系,那么不存在多方和一方再次关联的情况,是不适用这种方式的。

另外数据库也存在如果设置外键不能为空,不能更新的问题。

 

B)一方list中remove,并且多方显示delete

持久化代码:

User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
user.getContactInfos().remove(deletedContact);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

JPA执行过程:

1、remove操作把此条记录的user_id更新为null。

update contact_info set user_id=null where user_id=? and id=?

2、显式delete方法彻底删除多方的数据

delete from contact_info where id=?

 

分析:

1、更新外键为空,这是因为一方要维护关系。

2、删除多方数据,是因为显示调用了多方的delete方法。

如果我们想彻底删除掉多方的数据,这里其实做了一次无用的更新外键为空的操作。这个操作不但无用,而切一旦设置了外键不能为空,还会导致sql执行报错!

因此想彻底删除多方时,不要用这种方式(即一方不放弃维护关系)!

在这个实验中,我还做了个小测试,我把userRepository.save(user)可以去掉。发现程序正确执行,并且和去掉前的结果一样。我推断是因为此时持久化操作从多方delete发出,但是外键维护关系一方未放弃,还是会执行update的操作。

 

C)只在多方delete

持久化代码:

User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

JPA执行过程:

什么都没发生!

分析:

由于先进行了查询,所以jpa认为被删除的contactInfo数据和user的关系还在。直接删除contactInfo无效。必须先从一方持有的list中remove掉才行。

 

一方不放弃维护关系实验结论:

由于双方都维护外键关系,一方维护关系体现在对多方外键的更新上。而remove操作,只是断开关联。但不会删除多方数据。remove之后,多方显式调用delete操作,多方才会被删除。

在这种配置下,插入和删除,都会多执行一条update多方外键的sql,很多情况下是完全没必要的。而且如果数据库外键如果不能为空会报错。

适用场景:

1、多方的外键可以为空。也就是说多方和一方的关系是聚合,允许多方不关联一方。

2、只想update多方外键为空,而不想彻底删除多方数据。也就是3-A)的场景。

 

不适用场景:

1、想彻底删除多方数据,而且多方外键不能为空

 

一方放弃维护关系

关系配置代码

User
@Fetch(FetchMode.SUBSELECT)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user")
private List contactInfos = new ArrayList<>();

注:User中加上了mappedBy,代表user放弃维护外键关系

 

1、多方新增

A)没有给contactInfo设置user

 

持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().add(ContactInfo.builder()
        	.address("朝阳望京街道")
        	.phoneNumber("18612938250").build());
userRepository.save(user);

JPA执行过程:

只会新增一条userId为空的contactInfo

insert into contact_info (address, phone_number, user_id) values (?, ?, ?)

 

分析:

由于一方放弃维护关系,那么不会有update外键的操作。而由于设置了级联persist,所以多方数据会级联插入。但是导致插入的多方数据没有外键。如果数据库做了限制则会报错。

这种方式是错误的方式,即使成功插入也没有外键值。插入的数据和代码表述的含义不一致。

 

B)contactInfo设置user

持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().add(ContactInfo.builder()
        	.address("朝阳望京街道")
        	.phoneNumber("18612938250")
.user(user).build());
userRepository.save(user);

JPA执行过程:

新增contactInfo,user_id正常

insert into contact_info (address, phone_number, user_id) values (?, ?, ?)

分析:

1、由于多方放弃维护多方外键,所以新增的时候不会去更新外键。

2、但由于级联新增的设置,所以还是会插入多方数据。

3、多方需手动设置外键的关联对象,插入时外键才会有值。

这是一方放弃关系维护时,正确的多方插入姿势!!别忘了给插入的多方数据设置关联的一方对象!

 

2、多方更新

持久化代码:

User user=userRepository.findById(1L).get();
user.getContactInfos().get(0).setPhoneNumber("88888888");
userRepository.save(user);

JPA执行过程:

直接根据多方主键进行更新。和一方未放弃维护关系时一致

update contact_info set address=?, phone_number=?, user_id=? where id=?

分析:

由于更新前,先进行了查询,并且配置了双向关联,所以被更新的contactInfo数据是有关联user的,因此更新正常。

 

3、多方删除

A)仅从一方的list中remove

User user=userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(1);
user.getContactInfos().remove(deletedContact);
userRepository.save(user);

JPA执行过程:

什么都没有发生

分析:

remove操作只是使关系断开。但由于一方放弃外键关系维护,所以不会更新多方外键。而由于没有显式delete多方,所以也不会删除contactInfo数据。这种删除方式显然是错误的。

 

B)仅在多方delete

User user=userRepository.findById(1L).get();

ContactInfo deletedContact = user.getContactInfos().get(1);

contactInfoRepository.delete(deletedContact);

userRepository.save(user);

JPA执行过程:

什么都没有发生

分析:

由于先进行了查询,所以jpa认为被删除的contactInfo和user的关系还在。直接显式删除contactInfo无效。这种删除方式也是错误的。

 

C)从一方的list中remove,并且多方显式执行delete

User user=userRepository.findById(1L).get();

ContactInfo deletedContact = user.getContactInfos().get(1);

user.getContactInfos().remove(deletedContact);

contactInfoRepository.delete(deletedContact);

userRepository.save(user);

JPA执行过程:

根据主键直接删除掉contactInfo

delete from contact_info where id=?

结论:由于一方放弃了外键关系所以维护,所以remove的时候,一方不会去更新多方外键为null。在remove后关系断开,多方显式调用delete,可以删除掉contactInfo。

这是一方放弃关系维护时,正确的多方删除姿势!!别忘了先要在一方维护的多方list中remove掉删除数据,然后多方显式调用delete。

另外,去掉userRepository.save(user),删除操作也是可以正常被触发的。

 

实验总结

我先用表格的方式呈现实验结果:

  一方不放弃维护关系 一方放弃维护关系 不放弃时正确操作 放弃时正确操作 结论
多方新增

1、插入多方数据

2、更新主键

1、插入多方数据 如果数据库不允许多方外键为空,需要在多方设置好一方对象

1、多方设置一方对象

2、一方save

建议采用一方放弃方式,避免插入时执行两条sql
多方更新 直接根据多方主键进行更新 直接根据多方主键进行更新。 一方save 一方save 无区别
多方删除

A)

更新多方外键为空

B)

1、更新多方外键为空,

2、删除多方数据

直接删除多方数据

A)只从一方的list中remove多方

B)一方list中remove,并显式删除多方

一方list中remove,并显式删除多方

需要彻底删除多方数据时,建议一方放弃的方式。

如果不想删除多方,只想去掉外键,只能采用一方不放弃的方式

从上面总结可以看出,绝大多数场景下,应该采取一方放弃维护关系的方式。这避免了插入和删除时执行两条sql的问题,而且也不会因为数据库设置了外键字段不能为空,导致update的sql报错。新增时候,多方自己设置外键,一条insert语句搞定。删除时候也是一条delete语句搞定,效率更高。

只有在一方和多方是聚合关系,并且不想彻底删除多方的场景下,一方不放弃维护关系的方式才有用武之地。

其实看到最后,我们可以得出这样的结论:

一方设置mappedBy,放弃关系维护。这适用于绝大多数场景。

正确的多方新增方式:

手动在多方对象设置一方对象

正确的多方删除方式:

1、从一方维护的多方list中remove,

2、显式delete多方对象。

 

你可能感兴趣的:(Spring,Data,JPA)