SpringDataJPA级联更新保存报错org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.example.springbootsecurityconcise.bean.Role
多对多级联更新保存,使用数据库已有的id作为属性,修改级联属性为融合模式,
现象
对Spring Data JPA进行多对多级联保存,给User设置从数据库中查出的Role,然后报错
User
@Data
@Entity
@Table(name = "user")
public class User {
@Id
// 主键自动增长
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Integer id;
@Column(name = "username")
String username;
@Column(name = "password")
String password;
/**
* 多对多关系会在创建用户和新角色时级联新增,关联表为user_role,当前对象在关联表对应的外键,和另一方在关联表中对应的外键
* cascade:级联操作,如保存、删除时级联的行为
* joinColumns:在关联表中的外键名
* inverseJoinColumns:另一方在关联表中的外键名
*/
@ManyToMany(targetEntity = Role.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "user_role",
joinColumns = {@JoinColumn(name = "u_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "r_id", referencedColumnName = "id")})
List<Role> roles = new ArrayList<>();
}
Role
@Data
@Entity
@Table(name = "role")
public class Role{
@Id
// 自增
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Integer id;
@Column(name = "role")
String role;
// 用户角色多对多
@ManyToMany(mappedBy = "roles",fetch = FetchType.LAZY)
List<User> users = new ArrayList<>();
}
Test
@Test
void bcry() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
User user = new User();
user.setUsername("admin");
user.setPassword(bCryptPasswordEncoder.encode("1"));
// 查询数据库,使用已有角色
Role role = roleRepository.findById(1).get();
user.getRoles().add(role);
userRepository.save(user);
System.out.println(bCryptPasswordEncoder.encode("1"));
}
Console
org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.example.springbootsecurityconcise.bean.Role
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:289)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:229)
// ...
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.springbootsecurityconcise.bean.Role
at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:88)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:77)
原因
在设置多对多属性时,直接使用了级联属性CascadeType.ALL(拥有所有权限),导致在保存已有数据时,数据库中已经有这个id了,所以不能保存
- CascadeType.PERSIST
级联持久化(保存)操作:持久保存拥有方实体时,也会持久保存该实体的所有相关数据。这个属性就是造成上面问题的关键。当你保存一天条数据时,所有的关联数据都会进行保存,无论数据库里面有没有,但有时候我们是需要这样的级联操作的。- CascadeType.REMOVE
级联删除操作:删除当前实体时,与它有映射关系的实体也会跟着被删除。- CascadeType.DETACH
级联脱管/游离操作:如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。- CascadeType.REFRESH
级联刷新操作:假设场景 有一个订单,订单里面关联了许多商品,这个订单可以被很多人操作,那么这个时候A对此订单和关联的商品进行了修改,与此同时,B也进行了相同的操作,但是B先一步比A保存了数据,那么当A保存数据的时候,就需要先刷新订单信息及关联的商品信息后,再将订单及商品保存。- CascadeType.MERGE
级联更新(合并)操作:当Student中的数据改变,会相应地更新Course中的数据。- CascadeType.ALL
清晰明确,拥有以上所有级联操作权限。
解决
修改@ManyToMany(cascade = CascadeType.ALL)为@ManyToMany(cascade = CascadeType.MERGE)