-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 3 posts ] 
Author Message
 Post subject: Problem with duplicate entries on the join table
PostPosted: Mon Apr 21, 2008 4:56 pm 
Newbie

Joined: Thu Sep 13, 2007 2:45 pm
Posts: 10
Hi,

I have the following database tables:

Parent (id int primary key, name varchar(10))
Child (id int primary key, name varchar(10))
Parent_Child(id int primary key, parent_id int, child_id int)

In the Child POJO, I am trying to override .equals and hashCode taking
its name field as this field has to be unique.

I am trying to save Parent and Child using the following code:


Parent p = new Parent();
p.setName("p1");

Child c1 = new Child();
c1.setName("c1");

Child c2 = new Child();
c2.setName("c2");

Child c3 = new Child();
c3.setName("c3");

p.addChild(c1);
p.addChild(c2);
p.addChild(c3);

p = service.save(p);

At this point, I see records created on the tables like:

Parent:
1 p1

Child:
1 c1
2 c2
3 c3

Parent_Child
1,1,1
2,1,2
3,1,3

Up to this point, it looks fine.

I try to modify the child's name by working with the child object
that's returned to me (detached ones).

I am setting the names to c4, c5, c6.

if(p.getChildren() != null) {
int counter = 4;
for(Child c : p.getChildren()) {
c.setName("c"+counter);
counter++;
}
}

p = service.save(p);



What I see now on the tables are:

Parent
1 p1

Child:
1 c4
2 c5
3 c6

Hibernate runs the update query for all children records and update their name from c1 to c4, c2 to c5, c3 to c6.

After it runs the update, it runs inserts into Parent_Child with duplicate entries as follows.

Parent_Child
1,1,1
2,1,2
3,1,3
4,1,1
5,1,2
6,1,3

When I comment out equals() and hashCode(), then Hibernate doesn't run inserts after updates.



Here is the Parent class:
@Entity
@Table(name = "Parent")
public class Parent implements Serializable {

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


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



@OneToMany(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
@Cascade(value={org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
@JoinTable(name = "parent_child", inverseJoinColumns = { @JoinColumn(name = "child_id") }, joinColumns = @JoinColumn(name = "parent_id"))
private Set<Child> children = new HashSet<Child>();



public void addChild(Child c){
children.add(c);

}

}

The child class:


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

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

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



@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Child))
return false;
final Child other = (Child) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}

}


Can anyone explain me why it inserted duplicates records into Parent_Child though Hibernate updated children with new names?

If it inserted Child(ren), it makes sense to insert new records into Parent_Child.

If I comment out equals and hashcode, it works fine.

I think I overrode equals and hashcode correctly to have the child name unique.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Apr 22, 2008 7:26 am 
Newbie

Joined: Sat Oct 28, 2006 6:16 am
Posts: 17
I'm sorry I can only theorize as to why the inserts are run however I do know that your problem is probably caused by updating the elements in a set in such a way the the hashcode and equals of these elements is affected.

This is from the java.util.Set javadoc http://java.sun.com/javase/6/docs/api/java/util/Set.html

"Note: Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set."

In the code you submitted you have changed the value of the name property. As the name property is used to determine the hashCode of the child object the set no longer works as you expect.

To demonstrate this consider the following code:


Code:
if(p.getChildren() != null) {
    int counter = 4;
    for(Child c : p.getChildren()) {
        System.out.println(p.getChildren().contains(c));
        c.setName("c"+counter);
        System.out.println(p.getChildren().contains(c));
        counter++;
    }
}


I would expect this program to output (for the three children in your example)

Code:
true
false
true
false
true
false


So what is happening here? Well during each iteration of the loop the name property of each element is changed. Changing the name property affects the hashcode of the element in such a way that the Set p.getChildren() starts to think it doesn't contain this particular element anymore.

A solution may be to remove the element out of the list and then add it back again after the name element has been set. The code to do this could be:

Code:
if(p.getChildren() != null) {
    Set<Child> modifiedChildren = new HashSet<Child>();
    int counter = 4;
    for(Iterator<Child> i = p.getChildren().iterator(); i.hasNext()) {
        Child c = i.next();
        i.remove();
        c.setName("c"+counter);
        modifiedChildren.add(c);
        counter++;
    }
    p.getChildren().addAll(modifiedChildren);
}




However this is not very tidy.

A more elegant solution is to base the hashcode and equals off the id of the child and then maybe add some other code to do some validation of the set before you save it. Something like:

Code:
Set<String> childNamesEncounted = new HashSet<String>();
for (Child c : p.getChildren()) {
    if (childNamesEncounted.contains(c.getName())) {
        throw new RuntimeException("duplicate name entered");
    }
}


If you need to do this at the database level then a database constraint should also be implemented (not sure if it is possible to get hibernate to generate this constraint for you - probably faster to implement the constraint yourself).

I hope that helps.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Apr 22, 2008 12:44 pm 
Newbie

Joined: Thu Sep 13, 2007 2:45 pm
Posts: 10
Hi,
I tried the following approach and still I saw duplicates though I was trying to remove old elements and add new elements.

I guess setting a db constraint may be good.

Thanks,



A solution may be to remove the element out of the list and then add it back again after the name element has been set. The code to do this could be:

Code:
if(p.getChildren() != null) {
    Set<Child> modifiedChildren = new HashSet<Child>();
    int counter = 4;
    for(Iterator<Child> i = p.getChildren().iterator(); i.hasNext()) {
        Child c = i.next();
        i.remove();
        c.setName("c"+counter);
        modifiedChildren.add(c);
        counter++;
    }
    p.getChildren().addAll(modifiedChildren);
}




Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 3 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.