I got a problem with a many to many association in my persistence layer. My scenario is the following:
A user can has several roles and a role can have several user attached to it. During the tests I encountered a strange behavior. I created role object and several user objects. The role was set to each of the users. After this the users were saved using a DAO. Then one of the user gets loaded to check whether he got the role that was passed to him before saving the user object. Calling getRoles()
on the user shows that the role was set correctly.
To check whether the inverse direction also works the role object gets loaded from the database using a role DAO. But calling getUsers()
on the role object just returns an empty set, although it should contain all the users with this role.
I double checked the database table but everything seems all right. User, role and user_role table were all filled correctly.
So why doesn't the role object contain any user?
I'm using Hibernate and Spring with the following classes.
User class
@Entity
@Table
public class User extends BusinessObject {
...
// Option 1
@ManyToMany(fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
targetEntity=Role.class)
@JoinTable(name= "user_role",
joinColumns = {@JoinColumn(name="user_id")},
inverseJoinColumns = {@JoinColumn(name="role_id")})
// Option 2
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name= "user_role",
joinColumns = {@JoinColumn(name="user_id")},
inverseJoinColumns = {@JoinColumn(name="role_id")})
private Set<Role> roles = new HashSet<Role>();
...
}
Role class
@Entity
@Table
public class Role extends BusinessObject {
...
// Option 1
@ManyToMany(fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
mappedBy= "roles",
targetEntity = User.class)
// Option 2
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
开发者_如何学C @JoinTable(name= "user_role",
joinColumns = {@JoinColumn(name="role_id")},
inverseJoinColumns = {@JoinColumn(name="user_id")})
private Set<User> users = new HashSet<User>();
...
}
To test I'm using the following code in a JUnit test class.
@Test
public void test(){
Transaction trans = sessionFactory.getCurrentSession().beginTransaction();
Role userAdminRole = new Role();
userAdminRole.setName(RoleName.USER_ADMIN);
Role userRole = new Role();
userRole.setName(RoleName.USER);
User user1 = new User();
user1.setEmail("user1@user.de");
user1.getRoles().add(userAdminRole);
user1.getRoles().add(userRole);
userDao.save(user1);
User user2 = new User();
user2.setEmail("user2@user.de");
user2.getRoles().add(role);
userDao.save(user2);
User user3 = new User();
user3.setEmail("user3@user.de");
user3.getRoles().add(role);
userDao.save(user3);
trans.commit();
User loadedUser = userDao.load(user1.getId());
// Tests passes
Assert.assertNotNull(loadedUser);
Assert.assertEquals(user1, loadedUser);
Set<Role> roles = loadedUser.getRoles();
// Tests passes
Assert.assertEquals(2, roles.size());
Role loadedUserAdminRole = roleDao.findByName(RoleName.USER_ADMIN);
Set<User> users = loadedUserAdminRole.getUsers();
// Test fails: Count is 0 instead of 3 !!!!!!!
Assert.assertEquals(3, users.size());
}
UPDATE
Sorry I forgot to mention one thing. When I tested the code I of course didn't mark the many to many association twice in each class file. Instead I used either option 1 or option 2 in each class file.
The problem probably comes from the fact that you mapped the same bidirectional association twice. If you tell Hibernate twice about the same join table or join column, there is a problem. In a bidirectional association, one of the ends of the association must map the association, and the other one must tell Hibernate that it's the inverse of the other end, using the mappedBy
attribute.
Since a many-to-many is completely symmetric, choose one of the end to be the owner (i.e. the end which maps the association, and thus have the @JoinTable
annotation). The other side is just the inverse, and thus doesn't have a @JoinTable
annotation, but has a mappedBy
attribute.
Example:
@Entity
@Table
public class User extends BusinessObject {
...
// This end is the owner of the association
@ManyToMany
@JoinTable(name= "user_role",
joinColumns = {@JoinColumn(name="user_id")},
inverseJoinColumns = {@JoinColumn(name="role_id")})
private Set<Role> roles = new HashSet<Role>();
...
}
@Entity
@Table
public class Role extends BusinessObject {
...
// This end is not the owner. It's the inverse of the User.roles association
@ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<User>();
...
}
Additional notes:
- targetEntity isn't useful, since Hibernate knows it thanks to the generic type of the
Set
. It would be useful if the Set was aSet<SomeInterface>
- CascadeType.ALL is certainly not what you want. Do you want to delete all the roles of a user when deleting a user? What should happen to the other users having these roles?
- You should almost always initialize both ends of a bidirectional association. Hibernate takes into account the owner end (i.e. the end without the
mappedBy
attribute) to persist the association. - All of this is explained in the Hibernate reference documentation. Read it: it's full of useful information, and is not hard to understand.
When dealing with a bidirectional many-to-many association you have to maintain both ends of the association. In your case, you have to add the user to the role as well. Adding the role to the user isn't sufficient to establish a bidirectional association as you can read in book Java Persistance with Hibernate:
As always, a bidirectional association (no matter of what multiplicity) requires that you set both ends of the association.
精彩评论