遇到的问题
首先我在用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权限操作。应该根据业务需求选择所需的级联关系。否则可能酿成大祸。
级联的属性:
- CascadeType.PERSIST
级联持久化(保存)操作:持久保存拥有方实体时,也会持久保存该实体的所有相关数据。这个属性就是造成上面问题的关键。当你保存一天条数据时,所有的关联数据都会进行保存,无论数据库里面有没有,但有时候我们是需要这样的级联操作的。 - CascadeType.REMOVE
级联删除操作:删除当前实体时,与它有映射关系的实体也会跟着被删除。 - CascadeType.DETACH
级联脱管/游离操作:如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。 - CascadeType.REFRESH
级联刷新操作:假设场景 有一个订单,订单里面关联了许多商品,这个订单可以被很多人操作,那么这个时候A对此订单和关联的商品进行了修改,与此同时,B也进行了相同的操作,但是B先一步比A保存了数据,那么当A保存数据的时候,就需要先刷新订单信息及关联的商品信息后,再将订单及商品保存。 - CascadeType.MERGE
级联更新(合并)操作:当Student中的数据改变,会相应地更新Course中的数据。 - CascadeType.ALL
清晰明确,拥有以上所有级联操作权限。