-
Notifications
You must be signed in to change notification settings - Fork 176
Comparing cloned lists #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Can you elaborate on what's wrong with the equals method of the objects in the lists? java-object-diff relies heavily on the constraints of the standard collection types, which in turn expect items to implement A If you can't make equals work on the objects directly, maybe you can wrap them in a decorator that implements it properly? If that doesn't work, do you have an idea what needs to be done to deal with this kind of scenario? I'd be happy to hear your thoughts, because right now, I'm not really sure. However, maybe I don't yet fully understand your problem. If you could come up with a nice little (executable) example or even better, a unit test, that reproduces the problem, that would make it much clearer to me. |
My problem is that in my case The problem results if I compare 2 object graphs. Even if nothing is changed, the id's will be different in the cloned version. I annotated the getter methods of the id fields with Your idea to wrap the objects in a decorator is one of the best solutions for my problem that remains on the list. I could perhaps implement an generic decorator that implements equals and hashcode only by comparing the unexcluded 'not annotated with I have made some unit tests to reproduce my problem. Perhaps it is helpful to understand the problem: package de.eis.fba2.util;
import de.danielbechler.diff.introspection.ObjectDiffProperty;
import de.danielbechler.diff.node.DiffNode;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class DiffToolTest {
public static class Foo {
private Integer id;
private String text;
private Integer number;
private List<Bar> bars;
public Foo() {
}
public Foo(Foo other) {
this.id = other.id;
this.text = other.text;
this.number = other.number;
this.bars = cloneFooList(other.bars, Bar.class, this);
}
@ObjectDiffProperty(excluded = true)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@ObjectDiffProperty(excluded = false)
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@ObjectDiffProperty(excluded = false)
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
@ObjectDiffProperty(excluded = false)
public List<Bar> getBars() {
return bars;
}
public void setBars(List<Bar> bars) {
this.bars = bars;
}
public void resetIdRecursive() {
this.id = null;
for(Bar bar : getBars()) {
bar.resetIdRecursive();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Foo foo = (Foo) o;
// Equals is implemented on field id because id contains the primary identifier from the database table
if (id != null ? !id.equals(foo.id) : foo.id != null) return false;
return true;
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}
public static class Bar {
Integer id;
Foo foo;
String text;
Integer number;
public Bar() {
}
public Bar(Bar other) {
this.id = other.id;
this.foo = other.getFoo();
this.text = other.text;
this.number = other.number;
}
public Bar(Bar other, Foo parent) {
this(other);
this.foo = parent;
}
@ObjectDiffProperty(excluded = true)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Foo getFoo() {
return foo;
}
public void setFoo(Foo foo) {
this.foo = foo;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public void resetIdRecursive() {
this.id = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Bar bar = (Bar) o;
// Equals is implemented on field id because id contains the primary identifier from the database table
if (id != null ? !id.equals(bar.id) : bar.id != null) return false;
return true;
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}
public static <E> List cloneFooList(List<E> source, Class<E> sourceClass, Foo parent) {
return cloneChildList(source, sourceClass, parent, parent.getClass());
}
private static <E, F> List<E> cloneChildList(List<E> source, Class<E> childClass, F parent, Class<? extends F> parentClass) {
Constructor<? extends E> constructor;
try {
constructor = childClass.getConstructor(childClass, parentClass);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
ArrayList<E> result = new ArrayList<E>();
if (source != null) {
for (E element : source) {
try {
result.add(constructor.newInstance(element, parent));
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
return result;
}
Foo foo;
Bar bar1, bar2;
@Before
public void setUp() throws Exception {
foo = new Foo();
foo.setId(1);
foo.setNumber(4711);
foo.setText("foo for ever");
bar1 = new Bar();
bar1.setId(10);
bar1.setNumber(815);
bar1.setText("bar for ever 1");
bar1.setFoo(foo);
bar2 = new Bar();
bar1.setId(11);
bar1.setNumber(816);
bar1.setText("bar for ever 2");
bar1.setFoo(foo);
foo.setBars(Arrays.asList(bar1, bar2));
}
@Test
public void testBarUntouchedAfterClone() throws Exception {
bar1 = new Bar();
bar1.setId(10);
bar1.setNumber(815);
bar1.setText("bar for ever 1");
Bar clonedBar = new Bar(bar1);
clonedBar.setId(bar1.getId() + 1);
DiffNode diffNode = DiffTool.compareObjects(clonedBar, bar1);
assertEquals(DiffNode.State.UNTOUCHED, diffNode.getState());
}
@Test
public void testUntouchedAfterClone() throws Exception {
// Clone Object
Foo clonedFoo = new Foo(foo);
// Clear IDs, so em.merge() will give the clones new IDs
clonedFoo.resetIdRecursive();
// Simulation new IDs in Test Case
clonedFoo.setId(2);
clonedFoo.getBars().get(0).setId(20);
clonedFoo.getBars().get(1).setId(21);
DiffNode diffNode = DiffTool.compareObjects(clonedFoo, foo);
assertEquals(DiffNode.State.UNTOUCHED, diffNode.getState());
}
@Test
public void testChangedAfterClone() throws Exception {
// Clone Object
Foo clonedFoo = new Foo(foo);
// Clear IDs, so em.merge() will give the clones new IDs
clonedFoo.resetIdRecursive();
// Simulation new IDs in Test Case
clonedFoo.setId(2);
clonedFoo.getBars().get(0).setId(20);
clonedFoo.getBars().get(1).setId(21);
// Change something
clonedFoo.getBars().get(0).setText("changedBar");
DiffNode diffNode = DiffTool.compareObjects(clonedFoo, foo);
assertEquals(DiffNode.State.CHANGED, diffNode.getState());
}
}
public class DiffTool {
public static DiffNode compareObjects(Object now, Object before) {
Object realNow, realBefore;
if(now instanceof HibernateProxy) {
realNow = ((HibernateProxy) now).getHibernateLazyInitializer()
.getImplementation();
} else {
realNow = now;
}
if(before instanceof HibernateProxy) {
realBefore = ((HibernateProxy) before).getHibernateLazyInitializer()
.getImplementation();
} else {
realBefore = before;
}
return ObjectDifferBuilder
.startBuilding()
.inclusion().exclude().type(org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.class).and()
.inclusion().exclude().propertyName("hibernateLazyInitializer").and()
.inclusion().exclude().propertyName("erstelltAm").and()
.inclusion().exclude().propertyName("erstelltDurch").and()
.inclusion().exclude().propertyName("geaendertAm").and()
.inclusion().exclude().propertyName("geaendertDurch").and()
.inclusion().exclude().propertyName("vlc").and()
.filtering().returnNodesWithState(DiffNode.State.ADDED).and()
.filtering().returnNodesWithState(DiffNode.State.CHANGED).and()
.filtering().returnNodesWithState(DiffNode.State.REMOVED).and()
.filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and()
.introspection().setDefaultIntrospector(new HibernateLazyIntrospector()).and()
.build()
.compare(realNow, realBefore);
} |
Does your problem go away if you remove the db generated ID from equals and hashCode? If so, there's your solution. Whether this is preferable or not is not suitable for this list but if you skim some of the Hibernate/JPA books or google JPA entities equals/hashCode you'll see lots of similar recommendations.
|
@massfords suggestion sounds promising. I just found this answer on stackoverflow, which adds some more insights (e.g. a link to the Hibernate docs which addresses the same issue.)
They make it sound as if its easy to override the default equals/hashCode implementations, but I can't say for sure, since I never really used Hibernate. Can you give it a try, @K-Menki? |
Ok, I have found another solution. I converted my entity graph to a presentation graph and implemented the Thanks for your help 👍 |
Great! I'm glad you found a solution that works. And thanks for sharing it with us! |
Hello,
thanks for this really helpful project. It really helped me!
Now to my question:
I have created a clone from a graph of JPA mapped entities to implement a versioning of that graph. When I compared the cloned one with the original one, I saw that every list inside my root object was marked as changed. After analysing the problem, I realized that the equals() implementations of the objects inside the lists are the reason for that. The CollectionDiffer uses the equals() Methods of these Objects inside the Collections.filteredCopyOf() Method.
I tried to implement a ComparisonStrategy, but I was unhappy about the missing differDispatcher, so I stopped this approach.
How would you describe a best practice for my problem (Collection Diff only comparing unexcluded fields)?
Best regards
Kim
The text was updated successfully, but these errors were encountered: