1

I have a user class and role class and user role class . Now every time i am trying to add a new user with a set of role which is already existing it throws a Unique error which is correct . But ideally it should not try to save the new role if it already exists . Below i am adding all my tables and save method .

@Table(name = "t_user")
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "mobile_number")
    private String mobileNumber;

   
    @Column(name = "email")
    private String email;

   
    @Column(name = "first_name")
    private String firstName;

    @Size(max = 100)
    @NotBlank(message = "Last name can not be empty")
    @Column(name = "last_name")
    private String lastName;

    @Column(name = "is_archived")
    private Boolean isArchived = false;

    @Column(name = "qualification")
    private String qualification;

    @JsonIgnore
    @Column(name="password")
    private String password;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "t_user_role", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
            inverseJoinColumns = {
                    @JoinColumn(name = "role_id", referencedColumnName = "id")})
    private Set<Role> roles = new HashSet<>();


}

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

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name="name")
    private String name;

}

@Data
@Table(name = "t_user_role")
@Entity
public class UserRole {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "role_id")
    private Role role;
}

method where i am saving the user:

User newUser = new User();
        newUser.setFirstName(user.getFirstName());
        newUser.setLastName(user.getLastName());
        newUser.setEmail(user.getEmail());
        newUser.setMobileNumber(user.getPassword());
        newUser.setPassword(bcryptEncoder.encode(user.getPassword()));
        newUser.setRoles(user.getRoles());
        return userRepository.save(newUser);
    }

and below is the post request format to create the user:

{
    "firstName":"first",
    "lastName":"name",
    "email":"[email protected]",
    "mobileNumber":"1110122223",
    "password":"1234567890",
    "roles":[{
        "name":"ADMIN"
    }]
}

I do not want to insert the role if present which should be ideal . But this is the standard way i find while implementing spring security with roles

6
  • Can you add the exception ? We can't have idea without it Commented Jul 3, 2020 at 13:31
  • Lombok @ Data incorporates @ EqualsAndHashCode (needed for a Set). You do not want that. In your class, typ alt-insert and implement Equals() and HashCode(). Could be there's another problem, but that caught my eye. thorben-janssen.com/… Commented Jul 3, 2020 at 13:39
  • The exception coming is data integrity violation exception as the role admin already exists Commented Jul 3, 2020 at 13:47
  • Making a call using save method will insert the record in the database and due to unique constraint it will throw an error. If you want to prevent duplicate values and then you may check before the making a call save method and show your custom message Commented Jul 3, 2020 at 15:38
  • But I am making a join call on t_user_role table and not on the role table Commented Jul 3, 2020 at 15:47

2 Answers 2

1
+50

Your request is missing id of the role. As id is not present. Spring try to add a new role in role table.

{
    "firstName":"first",
    "lastName":"name",
    "email":"[email protected]",
    "mobileNumber":"1110122223",
    "password":"1234567890",
    "roles":[{
        "id" : "" // ID must be present here.
        "name":"ADMIN"
    }]
}

Or from the role -> name, you can fetch Role entity/obj from the role table and set it in User object.

[Update 2]:

@ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
  @ToString.Exclude
  private Set<Role> roles = new HashSet<>();

You need to change the cascade type from 'ALL' to 'DETACH'. See: ALL means if you save USER, ROLE will also get saved, if you delete USER, role should also get delete. This is not what we want. You only need to use 'ROLE', not manipulate the 'ROLE' tables record in any way.

Sign up to request clarification or add additional context in comments.

4 Comments

org.hibernate.PersistentObjectException: detached entity passed to persist: com.user.model.Role. I am getting this exception while persisting the object
Just in the request body i added id which is present and tried to make a rest call
yes write, when you try to save user, spring automatically try to save role (if Id of role is not present -> try to add a new record), (if id is present -> try to update the record). We need to mark role field in user as 'DETACH'. To do so -> you need to change the cascade type inside @ManytoMany annotation on roles set. [cascade = CascadeType.DETACH]
Please add comment for downvoting the answer.
1

On behalf of what I understand, your requirements are:

  1. User entity
  2. Role entity, with each user having multiple roles
  3. If role is passed from client, you want to save the role only if it does not exist in your database, else you want to use the existing role (UPDATE: which as per comments and my opinion, is never an ideal thing to do)

In your case, I would suggest let Spring take care of the User->Roles relationship as follows:

public class User {

  ... all fields
  @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  @ToString.Exclude
  private Set<Role> roles = new HashSet<>();
}

public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
}

In Role repository, you would want a method: Optional<Role> findByName(String Name);

In between the layers (preferably in the service layer), try this:

public Map<String, Object> addUser(User user) {
    // perform validations

    user.getRoles().forEach(role -> {
        Role existing = roleRepository.findByName(role.getName())
                       .orElse(new Role(role.getName())); // using optional to create new role with name passed in from the client
        if (existing.getId() != null) user.setRole(existing); // UPDATE: NOT IDEAL
    });
    
    
    ... other tasks
    userRepository.save(user); // this all saves the correct role
    return yourResponseMap;
}

Other notes:

  1. We generally prefer to keep fetches Lazy, instead of Eager. But there are cases when you may need Eager retrieval so it depends on you.
  2. Letting Spring Data JPA handle third tables is better in terms of convenience in my opinion.
  3. org.hibernate.PersistentObjectException: detached entity passed to persist occurs when you're trying to save the role passed in from client directly without loading it on your application (see the service layer method for 'add user').
  4. Check this link, you might find it helpful.

6 Comments

There are 2 issue in your addUser function, first it doesn't support multiple roles in a user, which is a minor issue. Secondly, we should not add a new role while adding a User. Roles are usually pre-defined only. This seems in a category of major issue.
I apologies, but there is nothing helpful in your answer which helps to resolve the issue. You just added additional 'LAZY' loading. which is still not helpful in this scenario. A user must have roles. 'LAZY' we used in scenario where we have additional property and which could have large collection or which have tendency to grow large.
@AmanGarg from the question: "But ideally it should not try to save the new role if it already exists . Below i am adding all my tables and save method." Indicates somewhat he saves it when the roles is non-existent. I agree that he should not be saving this new role. Regarding handling multiple rows, I'm posting an update but definitely someone who's coding at this stage can identify how he wants to modify the code. This is not a plug and play solution that will serve all cases.
Then validation needs to be added. Cascade.ALL is invalid here.
Thanks for your feedback. As mentioned, you may not want to save roles which are not already in the database, although your question appeared like it was something you were trying to do. And contrary to not using cascade ALL, I'd argue it depends. Using ALL in this case is arguably not ideal, but once again it goes with your question. Say, if you have a different case, where you want to save comments under a user, you may want to cascade user operations to add/remove comments. I'd say to take a better approach and try recommended steps unless you really have to! @VatsalRahul
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.