遇到的问题
首先我在用springboot-jpa写一个多对多demo,进行插入数据的时候遇到了如下的问题:
detached entity passed to persist
大概的意思是该数据插入的时候,使用了级联表中已经有的数据,该条数据的id已经存在,无法继续插入,因此:detached entity passed to persist。
这个是什么问题产生的呢?
这个问题搞了很久,网上的说法也是千奇百怪,后来突然恍然一悟,为什么会要插入数据插不进去,可能会发生的操作是什么,突然就想明白,是做了多对多操作,jpa的多对多操作的特点就是需要做级联,而级联的时候就可能系统认为是插入数据,所有的数据都需要进行持久化,就算数据库里面已经有的数据也进行了再次持久化。后来找到了@ManyToMany,果然注解属性的级联权限设置了:cascade = CascadeType.ALL,其中CascadeType.ALL的级联权限中包括了CascadeType.PERSIST,这个就是罪魁祸首。
JPA多对多级联的demo
级联代码如下:
User.java
package cn.kt.securitytest2.domin; /** * Created by tao. * Date: 2021/10/27 10:39 * 描述: */ import lombok.Getter; import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; import java.io.Serializable; import java.util.*; @Entity @Getter @Setter @Table(name = "user") public class User implements UserDetails, Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username") private String username; @Column(name = "password") private String password; @Column(name = "enabled") private Boolean enabled; @Column(name = "locked") private Boolean locked; @Column(name = "expired") private Boolean expired; @Column(name = "credentialsexpire") private Boolean credentialsExpire; @ManyToMany(targetEntity = Role.class, fetch = FetchType.EAGER, cascade = CascadeType.MERGE) @JoinTable(name = "user_role", //joinColumns配置当前对象在中间表中的外键(第一个参数是中间表的字段,第二个参数是本表对应的字段) joinColumns = {@JoinColumn(name = "uid", referencedColumnName = "id")}, //inverseJoinColumns配置对方对象在中间表中的外键 inverseJoinColumns = {@JoinColumn(name = "rid", referencedColumnName = "id")} ) private Set<Role> roles = new HashSet<Role>(); }
Role.java
package cn.kt.securitytest2.domin; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.io.Serializable; import java.util.HashSet; import java.util.Set; /** * Created by tao. * Date: 2021/10/27 10:39 * 描述: */ @Entity @Getter @Setter @Table(name = "role") public class Role implements Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "nameen") private String nameEN; @Column(name = "namezh") private String nameZh; //多对多关系映射 @ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER) @JsonIgnore private Set<User> users = new HashSet<User>(); @Override public String toString() { return "Role{" + "id=" + id + ", nameEN='" + nameEN + '\'' + ", nameZh='" + nameZh + '\'' + '}'; } }
其中user和role对应的是两张数据库表,还有一张关联的中间表user_role.
JPA级联操作的详解
通过以上的代码可以看到,User和Role的级联权限是CascadeType.ALL。
但经过实践得出:不要随便给all权限操作。应该根据业务需求选择所需的级联关系。否则可能酿成大祸。
级联的属性:
1. CascadeType.PERSIST
级联持久化(保存)操作:持久保存拥有方实体时,也会持久保存该实体的所有相关数据。这个属性就是造成上面问题的关键。当你保存一天条数据时,所有的关联数据都会进行保存,无论数据库里面有没有,但有时候我们是需要这样的级联操作的。
2. CascadeType.REMOVE
级联删除操作:删除当前实体时,与它有映射关系的实体也会跟着被删除。
3. CascadeType.DETACH
级联脱管/游离操作:如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。
4. CascadeType.REFRESH
级联刷新操作:假设场景 有一个订单,订单里面关联了许多商品,这个订单可以被很多人操作,那么这个时候A对此订单和关联的商品进行了修改,与此同时,B也进行了相同的操作,但是B先一步比A保存了数据,那么当A保存数据的时候,就需要先刷新订单信息及关联的商品信息后,再将订单及商品保存。
5. CascadeType.MERGE
级联更新(合并)操作:当Student中的数据改变,会相应地更新Course中的数据。
5. CascadeType.ALL
清晰明确,拥有以上所有级联操作权限。