Hi,
I'm just getting started with Hibernate -- apologies in advance.
I have a many-many relationship of Users and Groups. The crux of the issue is this:
The following code has the expected result of removing the association from both sides:
Code:
myUser.getGroups().remove(myGroup);
myGroup.getMembers().remove(myUser);
However, when I try to encapsulate this into a convenience method in the User class, as follows:
Code:
public void removeGroup(Group group) {
this.groups.remove(group);
group.members.remove(this); // DOES NOT WORK
}
it fails to remove the user from the group. Debugging of the code reveals that something suspect is happening in the User.equals() method. The object passed in to equals() is a javassist proxy class, and it appears to be un-initialized. So when the equals() implementation compares the objects (in this case, comparing the username values), they are not equal, which ultimately seems to cause the group.getMembers().remove() to fail because the (proxy) user object does not appear to be a member of the set.
Is this behavior as expected, or am I doing something wrong?
I have not done much testing with various different Lazy Load and Fetch annotations, though it seems odd to me that these would affect the fundamental programming semantics that I'm seeing here.
Thanks in advance,
Jon
The full source code in question and the unit tests are copied below:
User class:
Code:
package org.mitre.hibernatetest.entity;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.apache.log4j.Logger;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.hibernate.annotations.Parameter;
@Entity
// "User" is a SQL reserved word, so call the table something else
@Table(name = "users")
public class User implements java.io.Serializable {
private static final long serialVersionUID = 5429145115928312017L;
private static Logger logger = Logger.getLogger(Group.class);
@Id
@GeneratedValue(generator = "tableGenerator")
@GenericGenerator(name = "tableGenerator", strategy = "org.hibernate.id.enhanced.TableGenerator",
parameters = {@Parameter(name = "segment_value", value = "user_seq")})
private Long id;
@Column(nullable = false, unique = true)
private String username;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(name = "group_members", joinColumns=@JoinColumn(name="user_id", nullable = false),
inverseJoinColumns=@JoinColumn(name="group_id", nullable = false))
private Set<Group> groups = new HashSet<Group>();
public Set<Group> getGroups() {
return groups;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public Long getId() {
return id;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof User)) {
return false;
}
final User other = (User) obj;
logger.info("this: " + this.username + " (" + this.getClass() + ")");
logger.info("other: " + other.username + " (" + other.getClass() + ")");
if ((this.username == null) ? (other.username != null) : !this.username.equalsIgnoreCase(other.username)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 3;
hash = 41 * hash + (this.username != null ? this.username.hashCode() : 0);
return hash;
}
public boolean addGroup(Group group) {
boolean added = this.groups.add(group);
if (added) {
group.addMember(this);
}
return added;
}
public boolean removeGroup(Group group) {
boolean removed = this.groups.remove(group);
if (removed) {
group.removeMember(this);
}
return removed;
}
}
Group class:
Code:
package org.mitre.hibernatetest.entity;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import org.apache.log4j.Logger;
@Entity
@Table(name = "groups")
public class Group implements Serializable {
private static final long serialVersionUID = -4453002379222933845L;
private static Logger logger = Logger.getLogger(Group.class);
@Id
@GeneratedValue(generator = "tableGenerator")
@GenericGenerator(name = "tableGenerator", strategy = "org.hibernate.id.enhanced.TableGenerator",
parameters = {@Parameter(name = "segment_value", value = "group_seq")})
private Long id;
//
@Column(name = "group_name", nullable = false)
private String groupName;
//
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "groups",
targetEntity = User.class)
private Set<User> members = new HashSet<User>();
public Group() {
}
public Group(String groupName) {
this.groupName = groupName;
}
public Set<User> getMembers() {
// return Collections.unmodifiableSet(members);
return members;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public Long getId() {
return id;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof Group)) {
return false;
}
final Group other = (Group) obj;
logger.info("this group: " + this.groupName);
logger.info("other group: " + other.groupName);
if ((this.groupName == null) ? (other.groupName != null) : !this.groupName.equalsIgnoreCase(other.groupName)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 23 * hash + (this.groupName != null ? this.groupName.hashCode() : 0);
return hash;
}
protected boolean addMember(User member) {
return members.add(member);
}
protected boolean removeMember(User member) {
boolean removed = members.remove(member);
logger.info("Removed member: " + removed);
return removed;
}
}
Test class (using Spring):
Code:
package org.mitre.hibernatetest;
import javax.annotation.Resource;
import static org.junit.Assert.*;
import org.junit.runner.RunWith;
import org.junit.Test;
import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.junit.After;
import org.junit.Before;
import org.mitre.hibernatetest.entity.Group;
import org.mitre.hibernatetest.entity.User;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/applicationContext.xml"})
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = false)
@Transactional
public class TestDomainObjects extends AbstractTransactionalJUnit4SpringContextTests {
@Resource
private SessionFactory sessionFactory;
private User bart;
private User lisa;
private Group users;
private Group admins;
private Long bartId;
private Long lisaId;
private Long usersId;
private Long adminsId;
@Before
public void loadData() {
Session sess = sessionFactory.getCurrentSession();
Transaction tx = sess.beginTransaction();
bart = new User();
bart.setUsername("Bart");
bartId = (Long) sess.save(bart);
lisa = new User();
lisa.setUsername("Lisa");
lisaId = (Long) sess.save(lisa);
users = new Group("USERS");
usersId = (Long) sess.save(users);
admins = new Group("ADMINS");
adminsId = (Long) sess.save(admins);
bart.addGroup(users);
bart.addGroup(admins);
lisa.addGroup(users);
sess.flush();
sess.clear();
bart = (User) sess.load(User.class, bartId);
lisa = (User) sess.load(User.class, lisaId);
users = (Group) sess.load(Group.class, usersId);
admins = (Group) sess.load(Group.class, adminsId);
}
@Test
public void testRemoval1() throws Exception {
bart.getGroups().remove(users);
users.getMembers().remove(bart);
assertFalse(bart.getGroups().contains(users));
assertFalse(users.getMembers().contains(bart));
}
@Test
public void testRemoval2() throws Exception {
bart.removeGroup(users);
assertFalse(bart.getGroups().contains(users));
assertFalse(users.getMembers().contains(bart)); // FAILS!!!
}
@After
public void cleanup() {
Session sess = sessionFactory.getCurrentSession();
sess.delete(bart);
sess.delete(lisa);
sess.delete(users);
sess.delete(admins);
sess.flush();
sess.clear();
}
}
Log output from the User.equals() method:
Code:
2009-09-01 17:28:24,667 INFO org.mitre.hibernatetest.entity.Group - this: Bart (class org.mitre.hibernatetest.entity.User)
2009-09-01 17:28:24,667 INFO org.mitre.hibernatetest.entity.Group - other: null (class org.mitre.hibernatetest.entity.User_$$_javassist_4)
2009-09-01 17:28:24,667 INFO org.mitre.hibernatetest.entity.Group - Removed member: false