SpringDataJPA级联更新保存报错org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.example.springbootsecurityconcise.bean.Role

发布时间 2023-08-25 10:50:41作者: 蔚然丶丶

多对多级联更新保存,使用数据库已有的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了,所以不能保存

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

解决

修改@ManyToMany(cascade = CascadeType.ALL)为@ManyToMany(cascade = CascadeType.MERGE)