What if I want to persist a "new" entity with an "old" associated entity in Spring Data Jpa? Let me show you what I mean
public Mono<ServerResponse> signUp(ServerRequest request) {
return request.bodyToMono(UserDto.class)
.map(userMapper::toUser)
.map(userService::encodePassword)
.map(userService::addDefaultRoles)
.map(userService::save)
.map(this::toAuthenticatedUpat)
.map(tokenService::generateTokenFor)
.transform(jwt -> ServerResponse.status(HttpStatus.CREATED).body(jwt, String.class));
}
The important parts are addDefaultRoles()
and save()
@Override
public User save(User user) {
return userRepository.save(user); // Spring Data's repository
}
@Override
@Transactional // the only @Transactional method in UserServiceImpl
public User addDefaultRoles(User user) {
Optional<Role> userRoleOptional = roleService.findByAuthority(Role.USER);
Role userRole = userRoleOptional.orElse(new Role(Role.USER));
userRole.addUser(user);
user.addRole(userRole);
return user;
}
// @Entity etc.
public class User implements UserDetails {
// ...
@ManyToMany(cascade = CascadeType.PERSIST) // note this
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> authorities;
// @Entity etc.
public class Role implements GrantedAuthority {
public static final String USER = "user";
// ...
@ManyToMany(mappedBy = "authorities")
private Set<User> users;
userRoleOptional
is empty, and both the new user and the new USER
role are persisted// Spring's SimpleJpaRepository
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) { // true
entityManager.persist(entity); // persist cascaded to role
return entity;
} else {
return entityManager.merge(entity);
}
}
USER
). entityInformation.isNew(entity)
is evaluated to true
, again, persist()
is invoked and cascaded to the role which is now deemed detached, and this happensCaused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.tokenservice.data.entity.Role
How do I avoid this?
My conclusion is:
if
check will evaluate to true
no matter what (if you persist a new entity)save()
ing a new entity with an associated detached entity will inevitably throwHere's a workaround. Remove the cascade...
@ManyToMany
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> authorities;
...and persist the role beforehand (if it doesn't exist yet) manually
@Override
@Transactional
public User addDefaultRoles(User user) {
Optional<Role> userRoleOptional = roleService.findByAuthority(Role.USER);
Role userRole = userRoleOptional.orElseGet(() -> roleService.save(new Role(Role.USER)));
userRole.addUser(user);
user.addRole(userRole);
return user;
}