Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to persist @ManyToMany relation - duplicate entry or detached entity

I want to persist my entity with ManyToMany relation. But i have some problem during persisting process.

My entities :

@Entity
@Table(name = "USER")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long userId;

    @Column(name = "NAME", unique = true, nullable = false)
    String userName;

    @Column(name = "FORNAME")
    String userForname;

    @Column(name = "EMAIL")
    String userEmail;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "USER_USER_ROLES", joinColumns = @JoinColumn(name = "ID_USER"), inverseJoinColumns = @JoinColumn(name = "ID_ROLE"))
    List<UserRoles> userRoles = new ArrayList<UserRoles>();

    // getter et setter
}

and

@Entity
@Table(name = "USER_ROLES")
public class UserRoles implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long userRolesId;

    @Column(unique = true, nullable = false, name = "ROLE_NAME")
    String roleName; 

    // getter et setter
}

Service code :

User user = new User();
UserRoles role;
try {
    role = userRolesServices.getUserRoleByName("ROLE_USER"); // find jpql - transaction
} catch (RuntimeException e) {
    LOGGER.debug("No Roles found");
    role = new UserRoles("ROLE_USER"); // create new
}
user.addUserRole(role);
user.setUserName(urlId);
user.setUserForname(fullName);
user.setUserEmail(email);
userServices.createUser(user); // em.persist(user) - transaction

First time, when I try to persist a User with UserRoles "ROLE_USER", no problem. User and UserRoles and join tables are inserted.

My problem is when I try to persist a second User with the same UserRoles. I check if the UserRoles exists by finding it (userRolesServices.getUserRoleByName(...)). If exists -> add this UserRoles to User list (id + role name) else i create a new one (only role name).

By when I try to persist the second User, i obtain the following exception : "detached entity to persist : .....UserRoles" (maybe because getUserRoleByName is performed in another transaction)

If I do not use getUserRoleByName (only *new UserRoles("ROLE_USER");*), i obtain the following exception : "...ConstraintViolation : Duplicated entry for 'ROLE_NAME' ..."

So, how to properly persist an entity with @ManyToMany relation ?

like image 594
Aure77 Avatar asked Mar 22 '12 12:03

Aure77


People also ask

How do you persist many to many relationships in JPA?

In JPA we use the @ManyToMany annotation to model many-to-many relationships. This type of relationship can be unidirectional or bidirectional: In a unidirectional relationship only one entity in the relationship points the other. In a bidirectional relationship both entities point to each other.

How can we avoid duplicate records in JPA?

To avoid having JPA persist the objects automatically, drop the cascade and use persist to manually add the objects to the context immediately after creation. Since a persistence context is basically a tricked-out WeakHashMap attached to a database, these approaches are pretty similar when it comes down to it.


2 Answers

For above problem I would say your entity relationship cascade is wrong. Consider this: A user can have multiple roles but there can be fixed number of roles that can exist in the system. So CASCADE ALL from User entity does not make any sense, since life cycle of UserRoles should not depend on User entity life cycle. E.g. when we remove User, UserRoles should not get removed.

detached entity to persist exception will only occur when you are passing object which has primary key already set to persist.

Remove cascade and your problem will be solved now only thing you will need to decide is how you are going to insert User roles. According to me there should be separate functionality to do so.

Also do not use ArrayList, use HashSet. ArrayList allows duplicates.

like image 197
Amit Deshpande Avatar answered Oct 09 '22 12:10

Amit Deshpande


I will provide my answer if anyone get same type of problem to me and the author.

Basically what I was facing was a situation when I had one table which was some kind of CONSTANT values. And the other would change, but it should map (many to many) to those CONSTANTS.

The exact problem is USERS and it's ROLES.

Diagram

Roles would be known and added on system startup, thus they should never get removed. Even if no user would have some Role it should still be in the system.

The class implementation, using JPA:

User:

@Entity
@Table(name = "USERS")
public class User{

    @Id
    private String login;
    private String name;
    private String password;

    @ManyToMany(cascade = {CascadeType.MERGE})
    private Set<Role> roles = new HashSet<>();

Role:

@Entity
@Table(name = "ROLE")
public class Role {

    @Id
    @Enumerated(value = EnumType.STRING)
    private RoleEnum name;

    @ManyToMany(mappedBy = "roles")
    private Set<User> users = new HashSet<>();

Usage

This setup will easily add/remove Role to User. Simply by passing an array, f.e.: user.getRoles().add(new Role("ADMIN")); and merge the user. Removing works with passing an empty list.

If you forget to add the Role before adding it to the user most likely you will get an error like:

javax.persistence.RollbackException: java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: com.storage.entities.Role@246de37e.

What and why

  • mappedBy attribute is added to the child Entity as described in the JPA Docs

If you choose to map the relationship in both directions, then one direction must be defined as the owner and the other must use the mappedBy attribute to define its mapping (...)

  • cascade = {CascadeType.MERGE} is added for proper cascades JPA Docs

Cascaded the EntityManager.merge() operation. If merge() is called on the parent, then the child will also be merged. This should normally be used for dependent relationships. Note that this only affects the cascading of the merge, the relationship reference itself will always be merged.

like image 42
Atais Avatar answered Oct 09 '22 11:10

Atais