I am trying to update records of Users with serveral Roles in 2 tables (User and UserRole) with hibernate. When I do the save it works properly, however, when I try to update, if the object contains a role that is stored in the database it fails. I didn't found a good answer but I guess it can't be done directly with the method "update", the bidirection should me implemented mannualy in the update isn't it?. I followed the structure of Spring Security for users.
This is my User class:
@Entity
@Table(name = "users", schema = "cmsdb")
public class User {
private String username;
private String password;
private boolean enabled;
private Set<UserRole> userRole = new HashSet<UserRole>(0);
public User() {
}
public User(String username, String password, boolean enabled) {
this.username = username;
this.password = password;
this.enabled = enabled;
}
public User(String username, String password,
boolean enabled, Set<UserRole> userRole) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.userRole = userRole;
}
@Id
@Column(name = "username", unique = true, nullable = false, length = 45)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Column(name = "password", nullable = false, length = 60)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Column(name = "enabled", nullable = false)
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@OneToMany(fetch = FetchType.EAGER, mappedBy = "user") //,cascade = {CascadeType.ALL}
@Cascade({CascadeType.SAVE_UPDATE,CascadeType.DELETE})
@OnDelete(action = OnDeleteAction.CASCADE)
public Set<UserRole> getUserRole() {
return userRole;
}
public void setUserRole(Set<UserRole> userRole) {
this.userRole = userRole;
}
}
This is my UserRole class:
@Entity
@Table(name = "user_roles", schema = "cmsdb",
uniqueConstraints = @UniqueConstraint(
columnNames = { "role", "username" }))
public class UserRole {
private Integer userRoleId;
private User user;
@ValidRole
private String role;
public UserRole () {
}
public UserRole(User user, String role) {
this.user = user;
this.role = role;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "user_role_id",
unique = true, nullable = false)
public Integer getUserRoleId() {
return userRoleId;
}
public void setUserRoleId(Integer userRoleId) {
this.userRoleId = userRoleId;
}
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "username", nullable = false)
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Column(name = "role", nullable = false, length = 45)
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String toString () {
return role;
}
@Override
public int hashCode() {
return userRoleId;
}
@Override
public boolean equals(Object obj) {
UserRole role = (UserRole) obj;
return (userRoleId == role.getUserRoleId());
}
}
These are my DAO methods:
// IT WORKS PROPERLY
@Override
@Transactional
public boolean createUser(User user) {
String hashedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(hashedPassword);
Set<UserRole> roles = new HashSet<UserRole> ();
for(UserRole role: user.getUserRole()) {
roles.add(new UserRole(user,role.getRole()));
}
user.setUserRole(roles);
sessionFactory.getCurrentSession().save(user);
return true;
}
// It doesn't work if it tries to update an User with an UserRole stored in the UserRole table in the database
@Override
@Transactional
public boolean updateUser(User user) {
// Get user by id
User userpersis = (User) getSessionFactory().getCurrentSession().load(User.class, user.getUsername());
// Primary key shouldn't be modified
userpersis.setEnabled(user.isEnabled());
userpersis.getUserRole().retainAll( user.getUserRole() );
userpersis.getUserRole().addAll( user.getUserRole() );
sessionFactory.getCurrentSession().update(userpersis);
return true;
}
The error is:
WARN : org.hibernate.engine.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
ERROR: org.hibernate.engine.jdbc.spi.SqlExceptionHelper - Duplicate entry 'ROLE_MAINTENANCE-asdf' for key 'uni_username_role'
WARN : org.hibernate.engine.jdbc.spi.SqlExceptionHelper - SQL Warning Code: 1062, SQLState: 23000 ...
Thanks in advance.
In User
, add orphanRemoval
to the cascading:
@Cascade(cascade={CascadeType.ALL}, orphanRemoval=true)
@OnDelete(action = OnDeleteAction.CASCADE)
public Set<UserRole> getUserRole() { ...
(see e.g. JPA 2.0 orphanRemoval=true VS on delete Cascade)
Then, instead of
Set<UserRole> roles = new HashSet<UserRole> ();
// Guess for roles, should be needed to load them also from the db
for(UserRole role : user.getUserRole()) {
roles.add(new UserRole(userpersis,role.getRole()));
}
userpersis.setUserRole(roles);
try
userpersis.getUserRole().clear(); // Let Hibernate know that we're really removing all the references
for(UserRole role : user.getUserRole()) {
userpersis.getUserRole().add(new UserRole(userpersis,role.getRole()));
}
Although in fact, you probably don't even want to delete all UserRoles and then insert all new data. You could also do something like
userpersis.getUserRole().retainAll( user.getUserRole() );
userpersis.getUserRole().addAll( user.getUserRole() );
This, however, requires that you implement equals()
and hashCode()
in UserRole
.
Edit: Does of course not work when, as in your case, the user
's role objects cannot be the same as the userpersis
's because of the backreference to the owning object.