Grails Domain Classes in Sets
Asked Answered
W

2

7

Is it a bad practice to use domain objects in Sets or as keys in Maps?

In the past I've done things like this a lot

Set<Book> someBooks = [] as Set
someBooks.addAll (Book.findAllByAuthorLike('%hofstadter%'))
someBooks.add (Book.findByTitleLike ('%eternal%'))

However I have noticed that I often encounter problems when findAllByAuthorLike might return a list of Hibernate Proxy objects com.me.Book_$$_javassist_128 but findByTitleLike will return a proper com.me.Book object. This causes duplicates in the set because the real object and the proxy are considered not equal.

I find I need to be extremely careful when using Sets of domain objects like this, and I get the feeling it might be something I should not be doing in the first place.

The alternative is of course to use a set/map of id's, but it makes my code verbose and prone to misunderstanding

Set<Integer> someBooks = [] as Set // a set of id's for books    

@Burt: I thought Grails domain classes already did this, at least so that equals/compare was done on class/id's rather than the object instance. Do you mean a special comparator for hibernate proxies?

return (this.class == obj.class && this.id == obj.id) || 
       (obj.class  == someHibernateProxy && this.id == obj.id)
Wilkison answered 9/6, 2011 at 2:41 Comment(1)
There is no good solution for equals() in Hibernate, unless you have an ever-immutable unique identifier. I.e., the generated id changes from 0 to db id when you save a new object. Probably that's why Grails don't take responsibility for making own bad decision, and just stick to JVM one.Limonene
B
9

It's not bad practice at all, but just like in a non-Grails application you should override equals and hashCode if you'll be putting them in hash-based collections (HashSet, HashMap, etc.) and also implement Comparable (which implies a compareTo method) if you're going to use TreeSet/TreeMap/etc.

Billowy answered 9/6, 2011 at 3:9 Comment(5)
With regards to my reply above, I can't see it as a solution because the javaAssist class's equals method would not respond in the same way.Wilkison
Try it :) Hibernate proxies intercept all methods except getId() and ensure that the instance is loaded. So equals/hashCode will work fine since they'll be called on the real domain class instance that's lazily loaded.Billowy
Very interesting. I'm still a bit unsure of why the error was caused in the first place then? If I call Book.get(1) multiple times, is it returning the same object (via clever cacheing) or is equals overwritten to compare on ID only?Wilkison
get() returns the same instance because Hibernate uses its Session as the 1st-level cache. It's the only method that automatically uses the cache but other methods have ways of enabling the query cache for that query. Dynamic finders will always return real instances - proxies are returned when you have lazy-loaded collections, e.g. static hasMany = [foos: Foo] - a Foo retrieved by get() won't be "equal" to one from the foos collection since that will be a proxy. Grails does not add equals or hashCode behavior, just id and version fields and a default toString()Billowy
I see. Thank you. Cached Foo instances + javaAssist magic explain everything I was seeing. I had assumed GORM was overwriting equals and hence was using Set<Foo> without the thought I would have used for normal Java/Groovy classes.Wilkison
S
2

Proper implementation of equals() and hashcode() in a Hibernate-backed situation is far from trivial. Java Collections require that hashcodes of objects and behaviour of equals() don't change, but the id of an object can change when it's a newly created object, and other fields can change for a wide variety of reasons. Sometimes you have a good unchangeable business id that you can use, but quite often that's not the case. And obviously default Java behaviour is also not suitable in a Hibernate situation.

The best solution I've seen is described here: http://onjava.com/pub/a/onjava/2006/09/13/dont-let-hibernate-steal-your-identity.html?page=2

The solution it describes is: initialize the id as soon as the object is created. Don't wait for Hibernate to assign an id. Configure Hibernate to use version to determine whether it's a new object. This way, id in unchangeable, and can be safely used for hashcode() and equals().

Sorrow answered 24/8, 2011 at 11:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.