Insertion in Many to Many unidirectional relationship in Spring JPA - java

I have two tables, Users, and Roles. Roles are like "ADMIN", "MANAGER","USER". Users can have of the roles. So in my java project, I have the user as
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id")
private Integer userId;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "user_roles", joinColumns = #JoinColumn(name =
"user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
//other data and getters and setters
}
My Role class is like
#Entity
#Table(name = "role")
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "role_id")
private Integer roleId;
#Column(name = "role_name",unique=true)
private String role;
//getters and setters
}
Let's say I have inserted manually 3 records to Roles
INSERT INTO `role` VALUES (1,'ADMIN');
INSERT INTO `role` VALUES (2,'MANAGER');
INSERT INTO `role` VALUES (3,'USER');
Now I want to insert to User table such that only user and user_roles(join table) get inserted:
Eg:
If i want to insert an user with userId=1 and role={ADMIN,MANAGER}, an entry in user table and 2 entries in user_roles table like (1,1) and (1,2) should get inserted. There should not be any insertion to the Roles table. How do I achieve this?
I tried with changing manytomany to onetomany... Also, I tried changing cascadeType.all to CascadeType.MERGE and detach... none of them worked correctly...
please help
//UPDATE:
Adding code related to create/update user
public User createUser(UserDto account) {
User newUser = new User();
newUser.setPassword(account.getPassword());
newUser.setUsername(account.getUsername());
Set<Role> roles = new HashSet<Role>();
Role role = roleRepository.findByRole(account.getRole());
if (role != null) {
role.setRole(account.getRole());
roles.add(role);
newUser.setRoles(roles);
User savedUser = save(newUser);
return savedUser;
}
return null;
}
#Transactional
public User save(User user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
try {
user = userRepository.save(user);
} catch (org.springframework.dao.DataIntegrityViolationException ex) {
log.error(ex.getMessage());
return null;
}
return user;
}
#Override
public User updateUser(String oldUserName, UserDto userDto) {
Optional<User> optionalUser = userRepository.findByUsername(oldUserName);
optionalUser.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// if (optionalUser != null) {
User user = optionalUser.get();
if (user != null) {
Set<Role> roleset = new HashSet<Role>();
if (userDto.getRole() != null && !userDto.getRole().isEmpty()) {
Role role = roleRepository.findByRole(userDto.getRole());
if (role != null) {
roleset.add(role);
}
}
user.setRoles(roleset);
user.setUsername(userDto.getUsername());
user = userRepository.save(user);
return user;
} else
return null;
}
When i tried with
#ManyToMany(cascade = {CascadeType.MERGE,CascadeType.PERSIST}, fetch = FetchType.EAGER)
#JoinTable(name = "user_roles", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
i get the following exception
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.techjava.springbootsecuritymysql.model.Role

The error detached entity passed to persist is caused by the role being detached from the entityManager. The reason for that is, that it is read outside the Transaction but the association in user_roles is inserted within the transaction.
To solve that problem simply add #Transactional to the createUser method. Then the Role is read in the same transaction and won't be detached:
#Transactional
public User createUser(UserDto account) {
//...
}
The other thing is, that you set the cascade on the #ManyToMany. This is not needed if only the associations should be automatically persisted and deleted with the User. They will always be automatically created and deleted no matter what the cascase options are.
Cascade would be needed when you for example want the Role itself to be automatically inserted with the User which I assume you do not want from your code sample. So just remove the cascade:
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "user_roles", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;

Related

JPA #JoinTable with composite (2 column) primary keys

In a spring-boot app, I've got the following entity definition:
#Data
#Entity
#Table(name = "users")
public class User {
#Id
#Column(nullable = false, name = "username", length = 100)
private String username;
#JoinTable(name = "userrole",
joinColumns = { #JoinColumn(name = "username") },
inverseJoinColumns = { #JoinColumn(name = "role") }
)
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Role> roles;`
I'm using Spring-data-jpa,Hibernate with H2 as the database.
The trouble is that spring-data-jpa, hibernate always generate/creates the join table (DDL) 'userrole' with a single column primary key. e.g. 'username'.
Hence, if records such as {'username', 'user_role'} and {'username', 'admin_role'} is inserted in the join table ('userrole'), the next insert fails with an error due to the 'duplicate' primary key.
I've tried using both columns in the above definition, as well as the following variation:
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
#JoinColumns({
#JoinColumn(name = "username"),
#JoinColumn(name = "role") })
private List<Role> roles;`
But that they resulted in the same or worse problems, e.g. and in the latter, even table creation fails because only a single column is used as primary key for the jointable. Role is simply another table with 2 columns 'role' and 'description', basically a role catalog.
How do we specify to JPA that the #JoinTable should use both 'username' and 'role' columns as composite primary keys?
edit:
I tried using an independent table/entity as suggested, thanks #Kamil Bęben
#Data
#Entity
#Table(name = "users")
public class User {
#Id
#Column(nullable = false, name = "username", length = 100)
private String username;
#OneToMany(
fetch = FetchType.EAGER,
cascade = CascadeType.ALL,
mappedBy = "username",
orphanRemoval = true
)
#ElementCollection
private List<UserRole> roles;
UserRole is defined as such
#Data
#NoArgsConstructor
#Entity
#Table(name = "userrole")
public class UserRole {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "userrole_seq")
Long id;
#Column(nullable = false, name = "username", length = 100)
private String username;
#Column(nullable = false, name = "role", length = 50)
private String role;
the repository for that user-roles join table is defined as
#Repository
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
UserRole findByUsernameAndRole(String username, String role);
List<UserRole> findByUsername(String username);
List<UserRole> findByRole(String role);
}
Admittedly, ugly, but that it works. And that somehow, it seemed to use the correct findByUsername() method to retrieve the roles as is relevant to the user, probably related to the 'mappedBy' clause. 'black magic'! There's lots more that I'd still need to find my way around JPA, Spring, Spring-data
edit2:
further update:
the original #JoinTable works as well.
But that the relations need to be specified as #ManyToMany
#ManyToMany(
fetch = FetchType.EAGER,
cascade = CascadeType.MERGE
)
#JoinTable(name = "usersroles",
joinColumns = { #JoinColumn(name = "username") },
inverseJoinColumns = { #JoinColumn(name = "role") }
)
private List<Role> roles = new ArrayList<Role>();
This creates 2 column primary keys as expected for the 'users-roles' table
Thanks to #Roman
If Role only has two columns, eg user_id and role, the way to map this in jpa would be as following
#ElementCollection
#CollectionTable(name = "user_roles", joinColumns = #JoinColumn(name = "user_id"))
#Column(name = "role")
List<String> roles = new ArrayList<>();
Otherwise, jpa really requires each entity's identifier and join columns to be separate columns, so Role entity would have to have columns like id, user_id and role_name. Could look like this .:
class Role {
#Id
Long id;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id");
User user;
String roleName;
// Other fields
}
And in the User entity
#OneToMany(mappedBy = "user") // user is Field's name, not a column
List<Role> roles = new ArrayList<>();
Further reading

Remove the first entity and its relationship without removing the second entity - Spring Data JPA

I have two entities, one of UserEntity and the other RoleEntity, the user can have multiple roles and the role can be used by multiple users, my entities look like:
#Entity
public class UsersEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false)
private Long id;
//...
#ManyToMany(mappedBy = "users")
private Set<RolesEntity> roles;
//...
// Getters and setters
}
#Entity
public class RolesEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false)
private Integer id;
#NotNull
#Enumerated(EnumType.STRING)
#Column(length = 20)
private RoleEnum name;
#JoinTable(name = "user_roles", joinColumns = {
#JoinColumn(name = "role_id", referencedColumnName = "id", nullable = false)}, inverseJoinColumns = {
#JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false)})
#ManyToMany
private List<UsersEntity> users;
}
Generally roles are fixed they don't change a lot. Now I have a service:
public void removeUser(Long id) {
if (userRepository.findById(id).isPresent()) {
userRepository.deleteById(id);
} else {
throw new IllegalArgumentException("User not found!");
}
}
My requirement is to remove only the user and not the roles related with this user, which mean remove the user and the relation ship. When I call the previews method I got.
org.postgresql.util.PSQLException: ERROR: update or delete on table "users" violates foreign key constraint "constraint_user_id" on table "user_roles"
Detail: Key (id)=(4) is still referenced from table "user_roles".
Is there any trick to solve this please?
You need to make any references to that UsersEntity to be null.
So basically what is the problem? While RolesEntity has a reference to that UsersEntity class you cannot delete that. The most trivial thing to do is to make a loop for each RolesEntity in your UsersEntity class and remove everything from it.
Then you can successfully delete that user from your db.
Check this out to get more info: How to remove entity with ManyToMany relationship in JPA (and corresponding join table rows)?
I solved my issue like this, I'm not sure if this is a best approach to solve this:
public void removeUser(Long id) {
Optional<UsersEntity> userById = usersRepository.findById(id);
if (userById.isPresent()) {
UsersEntity user = userById.get();
for (RolesEntity role : user.getRoles()) {
role.setUsers(null);
rolesRepository.save(role);
}
usersRepository.delete(user);
} else {
throw new IllegalArgumentException("User not found!");
}
}
I think you can do this by using CascadeType.REMOVE
#ManyToMany(cascade = CascadeType.REMOVE, fetch = FetchType.EAGER)
#JoinTable(
name = "users_roles",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id"))
private List<Role> roles;

JPA Entity structure

I have a login page where the data will be retrieved from database
User
#Setter
#Getter
#Entity
#Table(name = "USER_DETAILS")
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "USER_ID")
private Long id;
#Column(name = "USER_NAME")
private String userName;
#Column(name = "USER_PASSWORD")
private String userPassword;
#Transient
private Set<Role> roles;
#ManyToMany
#JoinTable(name = "USER_ROLE", joinColumns = #JoinColumn(name = "USER_ID"), inverseJoinColumns = #JoinColumn(name = "ROLE_ID"))
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
Role
#Setter
#Getter
#Entity
#Table(name = "USER_ROLE")
public class Role implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ROLE_ID")
private Long id;
#Column(name = "USER_NAME")
private String userName;
#Column(name = "ROLE_NAME")
private String roles;
#Transient
private Set<User>users;
#ManyToMany(mappedBy = "roles")
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
UserDetailServiceImpl
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
#Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
System.out.println("Name "+user.getUserName());
System.out.println("role is "+user.getRoles());
if(null == user) {
throw new UsernameNotFoundException("No user present with username: " + username);
}
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
for (Role role : user.getRoles()){
grantedAuthorities.add(new SimpleGrantedAuthority(role.getUserName()));
}
return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getUserPassword(), grantedAuthorities);
}
}
Table
Role
ROLE_ID | USER_NAME | ROLE_NAME
1 John Admin
User
USER_ID | USER_NAME | USER_PASSWORD | USER_ROLE
1 John pass Admin
Output
Name John role is null 2018-03-12 00:52:06.362 ERROR 12563 ---
[nio-8088-exec-8] w.a.UsernamePasswordAuthenticationFilter : An
internal error occurred while trying to authenticate the user.
org.springframework.security.authentication.InternalAuthenticationServiceException:
null
I can't get the role value. What's wrong with the database structure ?
For #ManyToMany to work, you need 3 tables not 2. The two parent tables are typically linked through a third table containing two foreign keys. So here, there should be a third table which would have role_id and user_id.
Also, instead of adding #ManyToMany annotation to the getters, try adding it at the time of declaration itself. Something like this in User class,
#ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name = "user_role",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
And the same modification in Role class:
#ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();
Here, user_role is the third table.

how to find that the list contains all elements from the another list?

Within my spring boot project I have a defined Chat and User entity that are related by many to many
Chat.java
#ManyToMany
#JoinTable(name = "chat_user",
joinColumns = #JoinColumn(name = "chat_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id"))
private Set<User> users = new HashSet<User>();
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
User.java
#ManyToMany
#JsonBackReference
#JoinTable(name = "chat_user",
joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "chat_id", referencedColumnName = "id"))
private Set<Chat> chats = new HashSet<Chat>();
public Set<Chat> getChats() {
return chats;
}
public void setChats(Set<Chat> chats) {
this.chats = chats;
}
how to write a method inside CrudRepository so that I get a Chat list that contains all users from the list of users
So far I have a written method that returns a chat list that contains any of the users in the user list
public interface ChatRepo extends CrudRepository<Chat, Long> {
List<Chat> findAllByUsers(User user);
List<Chat> findDistinctByUsersIn(Set<User> users);
}
implementing equals/hashCode like Malte Hartwig suggest is solution
#Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof User)) {
return false;
}
User user = (User) o;
return id == user.id &&
Objects.equals(firstName, user.firstName) &&
Objects.equals(lastName, user.lastName) &&
Objects.equals(userName, user.userName) &&
Objects.equals(password, user.password);
}
#Override
public int hashCode() {
return Objects.hash(id, firstName, lastName, userName, password);
}
There is no straightforward solution to this(from what I see). What you can do is
Create a method in your ChatRepo like this
public interface ChatRepository extends CrudRepository<Chat, Long> {
#Query("select c from Chat c WHERE :user in elements(c.users)")
List<Chat> getChatsWithUsers(#Param("user") User user);
}
This will get you the result of all the Chats which this particular user is part of.
And then you can do this for all the Users in your set and then do a intersection of the results.
For this to work you need to put #ElementCollection in your Chat class
..........
#ElementCollection
private Set<User> users = new HashSet<User>();
But I agree this is not a ideal solution because if the list is too big, then it results in multiple database calls
#ManyToMany
#JoinTable(name = "chat_user",
joinColumns = #JoinColumn(name = "chat_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "user_id", referencedColumnName =
"id"))
private Set<User> users = new HashSet<User>();
I think this is maybe wrong. Because one chat id cannot point out many user.
So you can use simple jointable to user entity

Delete ManyToMany link

I'm newbie in Hibernate and I've a question about it.
I've 3 tables: User, Role and UserRole.
User and Role have ManyToMany relationship mapped by UserRole.
I have 2 rows in Role: (1, ROLE_ADMIN) (2, ROLE_USER).
I have 1 row in User: (1, TESTUSER).
I have 2 rows in UserRole: (1,1)(1,2).
I have 2 Entities: UserEntity and RoleEntity.
I added 2 role to TESTUSER calling method .add( Role ) on UserEntity and after .save( User ) on Session.
I can add all role i want to TESTUSER and it's works!
When i call .remove( Role ) on UserEntity and after .save( User ) on my JPA repository. It doesn't work.
I mean that hibernate doesn't execute any query to delete elements by table UserRole. Why?
Can you help me?
RoleEntity
#Entity
#Table(name = "role" , uniqueConstraints= {
#UniqueConstraint(columnNames={ "role_id" }) ,
#UniqueConstraint(columnNames={ "name" })
})
#Indexed
public
class Role
implements Serializable
{
#ManyToMany( mappedBy="roleList", fetch = FetchType.LAZY )
#Fetch(FetchMode.SELECT)
#BatchSize(size = 100)
private List<User> userList = new ArrayList<>(0);
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "role_id", nullable = false)
private
Long roleId;
#Column(name = "name", nullable = false)
private
String name;
UserEntity
#Entity
#Table(name = "user" , uniqueConstraints= {
#UniqueConstraint(columnNames={ "user_id" }) ,
#UniqueConstraint(columnNames={ "name" })
})
#Indexed
public
class User
implements Serializable
{
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "user_role")
#Fetch(FetchMode.SELECT)
#BatchSize(size = 100)
private List<Role> roleList = new ArrayList<>(0);
#Column(name = "name", nullable = false)
private
String name;
My test code:
Role adminRole = new RuoloBuilder().setName("ROLE_ADMIN").build();
Role userRole = new RuoloBuilder().setName("ROLE_USER").build();
adminRole = roleService.saveOrUpdate(adminRole);
userRole = roleService.saveOrUpdate(userRole);
User user = new UtenteBuilder()
.setName("TESTUSER")
.setRoleList(Arrays.asList(adminRole, userRole))
.build();
user = userService.saveOrUpdate(user); // It works
user.getRoleList().remove(userRole);
userService.saveOrUpdate(user); // It doesn't work
#ManyToMany(fetch = FetchType.LAZY,cascade=CascadeType.ALL) // on the entity User
You have to cascade all the operations PERSIST, REMOVE, REFRESH, MERGE, DETACH to the linked entities (Roles).

Categories