2011-12-31 8 views
6

Tengo un problema con hashCode() que se delega en objetos no inicializados utilizando hibernate.Delegar función hash a delegados no inicializados en causas de hibernación cambiar hashCode

Mi modelo de datos se ve de la siguiente manera (el siguiente código es muy podada hacer hincapié en el problema y por lo tanto rompe, no se replican!):

class Compound { 
    @FetchType.EAGER 
    Set<Part> parts = new HashSet<Part>(); 

    String someUniqueName; 

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

class Part { 
    Compound compound; 

    String someUniqueName; 

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

Tenga en cuenta que la aplicación de hashCode() sigue cuidadosamente los consejos como se da in the hibernate documentation.

Ahora, si cargo un objeto del tipo Compound, carga ansiosamente el HasSet con las piezas. Esto llama al hashCode() en las partes, que a su vez llama al hashCode() en el compuesto. Sin embargo, el problema es que en este punto, no todos los valores que se consideran para crear el hashCode del compuesto están disponibles. Por lo tanto, el hashCode de las piezas cambia después de que se completa la inicialización, frenando así el contrato del HashSet y dando lugar a todo tipo de errores difíciles de localizar (como, por ejemplo, tener el mismo objeto en las piezas configuradas dos veces).

Así que mi pregunta es: ¿Cuál es la solución más simple para evitar este problema (me gustaría evitar escribir clases para la carga/inicialización personalizada)? ¿Hago algo mal aquí por completo?

Editar: ¿Falta algo aquí? Esto parece ser un problema básico, ¿por qué no encuentro nada al respecto en ningún lado?

En lugar de utilizar el identificador de base de datos para la comparación de igualdad, se debe utilizar un conjunto de propiedades de los iguales() que identifican sus objetos individuales. [...] No es necesario utilizar el identificador persistente, por lo que llamado "clave comercial" es mucho mejor. Es una clave natural, pero este tiempo no hay nada de malo en su uso! (article from hibernate)

Y

Se recomienda que implemente equals() y hashCode() utilizando negocios igualdad de clave. La igualdad de clave comercial significa que el método igual() compara solo las propiedades que forman la clave comercial. Es una clave que identificaría nuestra instancia en el mundo real (una clave candidata natural ). (hibernate documentation)

Editar: Este es el seguimiento de la pila cuando la carga pasa (en caso de que esto ayuda). En ese momento, el atributo someUniqueName es nulo y, por lo tanto, hashCode se calcula erróneamente.

Compound.getSomeUniqueName() line: 263 
Compound.hashCode() line: 286 
Part.hashCode() line: 123 
HashMap<K,V>.put(K, V) line: 372  
HashSet<E>.add(E) line: 200 
HashSet<E>(AbstractCollection<E>).addAll(Collection<? extends E>) line: 305 
PersistentSet.endRead() line: 352 
CollectionLoadContext.endLoadingCollection(LoadingCollectionEntry, CollectionPersister) line: 261 
CollectionLoadContext.endLoadingCollections(CollectionPersister, List) line: 246  
CollectionLoadContext.endLoadingCollections(CollectionPersister) line: 219 
EntityLoader(Loader).endCollectionLoad(Object, SessionImplementor, CollectionPersister) line: 1005 
EntityLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 993 
EntityLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857  
EntityLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274 
EntityLoader(Loader).loadEntity(SessionImplementor, Object, Type, Object, String, Serializable, EntityPersister, LockOptions) line: 2037  
EntityLoader(AbstractEntityLoader).load(SessionImplementor, Object, Object, Serializable, LockOptions) line: 86 
EntityLoader(AbstractEntityLoader).load(Serializable, Object, SessionImplementor, LockOptions) line: 76 
SingleTableEntityPersister(AbstractEntityPersister).load(Serializable, Object, LockOptions, SessionImplementor) line: 3293 
DefaultLoadEventListener.loadFromDatasource(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 496  
DefaultLoadEventListener.doLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 477  
DefaultLoadEventListener.load(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 227 
DefaultLoadEventListener.proxyOrLoad(LoadEvent, EntityPersister, EntityKey, LoadEventListener$LoadType) line: 269 
DefaultLoadEventListener.onLoad(LoadEvent, LoadEventListener$LoadType) line: 152  
SessionImpl.fireLoad(LoadEvent, LoadEventListener$LoadType) line: 1090 
SessionImpl.internalLoad(String, Serializable, boolean, boolean) line: 1038 
ManyToOneType(EntityType).resolveIdentifier(Serializable, SessionImplementor) line: 630 
ManyToOneType(EntityType).resolve(Object, SessionImplementor, Object) line: 438 
TwoPhaseLoad.initializeEntity(Object, boolean, SessionImplementor, PreLoadEvent, PostLoadEvent) line: 139 
QueryLoader(Loader).initializeEntitiesAndCollections(List, Object, SessionImplementor, boolean) line: 982 
QueryLoader(Loader).doQuery(SessionImplementor, QueryParameters, boolean) line: 857 
QueryLoader(Loader).doQueryAndInitializeNonLazyCollections(SessionImplementor, QueryParameters, boolean) line: 274 
QueryLoader(Loader).doList(SessionImplementor, QueryParameters) line: 2542 
QueryLoader(Loader).listIgnoreQueryCache(SessionImplementor, QueryParameters) line: 2276  
QueryLoader(Loader).list(SessionImplementor, QueryParameters, Set, Type[]) line: 2271 
QueryLoader.list(SessionImplementor, QueryParameters) line: 459 
QueryTranslatorImpl.list(SessionImplementor, QueryParameters) line: 365 
HQLQueryPlan.performList(QueryParameters, SessionImplementor) line: 196 
SessionImpl.list(String, QueryParameters) line: 1268  
QueryImpl.list() line: 102 
<my code where the query is executed> 
+0

* (no es una respuesta a su pregunta) * ... * "Tenga en cuenta que la implementación de hashCode sigue atentamente los consejos que figuran en la documentación de hibernación" * [sic]. El mismo artículo que enlazas compone los dos códigos hash multiplicando por un número primo antes de agregar, que es mucho más común que simplemente sumar. Te recomiendo que realmente sigas ese consejo y multipliques tu * compuesto * por un número primo antes de hacer la suma. O eso o hacer XOR los dos hashes. Pero simplemente agregarlos típicamente aumenta los riesgos de colisiones. – TacticalCoder

+0

Gracias por la recomendación. El código real lo hace. Sin embargo, esto no ayuda a ilustrar el problema, y ​​es por eso que lo eliminé. Como se explica en la nota anterior: "* el siguiente código es muy útil para enfatizar el problema *". – roesslerj

+0

No es la solución más ideal, pero podría resolver su problema. Para las diversas colecciones de hash, la primera llamada es a hashCode * como optimización *. Entonces se llama a igual() si los códigos hash son iguales. Podría hacer que hashCode() siempre devuelva 1, suponiendo que su método equals() no sufre el mismo problema (p. Ej., Un valor no inicializado de compound.someUniqueValue). ¿Cómo se ve el método equals()? – Saish

Respuesta

2

Tiene un caso de uso legítimo perfecto, y de hecho debería funcionar. Sin embargo, tendrías el mismo problema en Java normal, si configuraras las 'partes' del objeto compuesto antes de establecer el 'nombreUnique único'.

Así que si pudiera convencer a hibernate para establecer la propiedad 'someUniqueName' antes de la propiedad 'parts'. ¿Experimentó con solo reordenarlos en la clase java? ¿O renombrar 'partes' a 'zparts'? Los documentos de hibernación solo dicen que el pedido no está garantizado. Me presento un error en hibernación para permitir a cumplir esta orden ...

Otra solución que podría ser más fácil:

class Part { 
    public int hashCode() { 
    //don't include getCompound().hashCode() 
    return getSomeUniqueName() == null ? 0 : getSomeUniqueName().hashCode(); 
    } 

    public boolean equals(Object o) 
    { 
    if (this == o) return true; 
    if (!o instanceof Part) return false; 

    Part part = (Part) o; 

    if (getCompound() != null ? !getCompound().equals(part.getCompound()) : part.getCompound()!= null) 
     return false; 
    if (getSomeUniqueName()!= null ? !getSomeUniqueName().equals(part.getSomeUniqueName()) : part.getSomeUniqueName()!= null) 
     return false; 

    return true; 
    } 
} 

En los Compound.equals() Asegúrese de que también comienza con

public boolean equals(Object o) 
{ 
    if (this == o) return true; 

Esto debería evitar el problema que está teniendo ahora.

Cada propiedad en el método hashCode() debe estar en el método equals(), pero no necesariamente al revés.

+0

Me doy cuenta de que es lo opuesto al título de la pregunta que era: "Delegar función hash" :-( – greyfairer

+0

Traté de reordenar las propiedades y también cambiarles el nombre. .no cambié nada. Tengo la sensación de que las colecciones se cargan antes que cualquier otra propiedad. 'equals()' de hecho comienza con el recorte que usted dio, sin embargo, no veo cómo esto podría ayudar a mi problema? – roesslerj

+0

Tiré getCompound () .hashCode() fuera del hashCode() de la Parte, por lo que ya no está delegando :-). Eso debería evitar su problema. Lo mantuve en el mismo nivel para que tu lógica comercial no se rompa. – greyfairer

0

Una posibilidad pensé fue siguiendo los consejos que figuran en this article. Básicamente, proponen no usar hibernate (o más bien la base de datos) para generar ID, sino usar una biblioteca UUID para generar sus propios ID y luego usar estos ID para equals() y hashCode(). Además de los problemas mencionados en este artículo, tiene algunos inconvenientes serios para mi implementación actual: ¡romperá mi código existente!Cada vez que crearía un Part -instance, primero tendría que consultar si ya existe en la base de datos y recuperarlo si fuera así. En mi implementación actual, simplemente creo Parts como me gusta y simplemente las agrego al Compound. Si el Compound ya tiene esa parte, todo funciona automágicamente ...

0

Encontré una pregunta relacionada here. La idea básica de la solución es que todo el problema desaparezca tan pronto como no busque las partes con ansiedad. Entonces, el compuesto ya está completamente inicializado cuando se cargan las piezas. Sin embargo, esto abre un problema completamente diferente cuando se trabaja fuera de las sesiones con objetos separados ...

0

No estoy del todo seguro, pero puede intentar comprobar si el Objeto que Hibernate devuelve es realmente un Objeto que está buscando (. clase), instanceof en este caso no será un buen candidato porque el proxy es una subclase. Hibernate devolverá un proxy si no todos los miembros se resuelven y, por lo tanto, los códigos hash/iguales se romperán con seguridad.

Cheers, Eugene

+0

Gracias por la ayuda. Lamentablemente, este no es mi problema en este caso. – roesslerj

2

Desde su pregunta que entiende que todas las propiedades de su modelo, que cada vez que participan en hashCode() método no se cargan por defecto. En ese caso, si desea que se carguen todas sus propiedades, puede seguir ciertas formas.

  1. Llamando a los métodos getter en hashCode() de su clase de modelo, ya que inicializa/carga todas las propiedades del modelo.
  2. Al usar sesstion.get() en lugar del método session.load(), ya que no creará ningún proxy y cargará todas las propiedades de su modelo.
  3. Configurando lazy="false" para todas sus propiedades en el mapeo.

¡Espero que esto pueda resolver su problema!

+0

Gracias por la ayuda. En cuanto a 1: es el mismo propuesto por abahgat y lamentablemente no ayuda. Para 2: utilizo 'query.list()' para recuperar el objeto problemático mediante consulta. Y para 3: esto es lo que estoy haciendo actualmente y realmente parte del problema ... – roesslerj

+0

Lo siento, tal vez fui engañoso ... las propiedades que participan en 'hashCode()' ESTÁN cargadas, pero solo después de que hashCode ha calculado ... – roesslerj

2

Leí en uno de los comentarios a la pregunta que está dejando que Eclipse genere los métodos equals y hashCode.

¿Hiciste eso para todas las entidades (Part y Compound)? Lo estoy preguntando porque si ese es el caso, esos métodos generalmente acceden directamente a las propiedades del objeto (es decir, sin llamar a los métodos getter). Se ven como los siguientes.

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

Al utilizar Hibernate, que a menudo conduce a problemas como el que usted está describiendo, ya que las propiedades no inicializadas tienen valores por defecto (null de objetos, 0 para int s, y así sucesivamente) hasta que el método get apropiada se llama, lo que hace que el proxy de hibernación acceda a la base de datos y cargue los valores que necesita para calcular los valores adecuados para los métodos.

Puede detectar fácilmente el problema si inicia un depurador e inspecciona las propiedades en la primera llamada al hashCode().

Si eso sucede, la forma más fácil de solucionar este problema es modificar sus métodos de usar los métodos get, como aquí:

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

Otro punto a destacar: la generada en Eclipse es igual método contiene realiza esta comprobación getClass() != obj.getClass() que no es apropiado para entidades de Hibernate que son ampliadas por proxies de Hibernate. Reemplazaría eso con un cheque instanceof.

+1

+1 para la pista con la instancia de verificación. La otra cosa también suena razonable, lo comprobaré. ¡Gracias de cualquier manera! – roesslerj

+0

Lamentamos informar que esto no solucionó mi problema. Estoy dentro de los límites de una sesión de hibernación y llamo al método getter, y sigo obteniendo un valor nulo en lugar del valor real del atributo. ¿Hay algo más que pueda hacer mal en ese sentido? – roesslerj

+0

¿Obtiene nulo en todos los atributos o solo en algunos de ellos? Tal vez podría ser útil si publicó el código real para su código hash y es igual a los métodos. Solo adivinando. – abahgat

2

¿Por qué no utiliza una lista, en lugar de un conjunto? Sé que sería más bien una solución que una solución adecuada, pero no tendrías que jugar con hashcodes en absoluto.

+0

Gracias por la idea. Pero en esa situación específica, la implementación de 'hashCode()' y 'equals()' me permite simplemente agregar cualquier objeto recién creado al conjunto, y si ya está contenido, no pasa nada y no se conserva. Eso me asegura una gran cantidad de código adicional y/o consulta de bases de datos. Una mejor solución sería simplemente devolver '0' como' hashCode() 'y dejar' equals() 'como está, resultando lo mismo que una lista donde los valores iguales no se reemplazan al insertarlos. Sin embargo, la solución sugerida por greyfairer es mejor ... – roesslerj

+0

Bastante. Solo tenga en cuenta que no debe calcular hashCode() y equals() en campos que no son efectivamente definitivos. Además, no creo que deba incluir compuesto al calcular hashCode() o equals() para las partes, ya que esa es solo la referencia principal, no algo de lo que las partes están compiladas. – rmaruszewski

+0

Es cierto que las piezas no se construyen a partir de componentes, sin embargo, podría suceder que dos partes con los mismos atributos pertenezcan a dos componentes diferentes, en cuya situación el componente es la única forma de diferenciarlos. En mi situación (que es bastante común, creo), las partes no son únicas con respecto al "universo", sino solo con respecto a "su mundo", que es el compuesto. – roesslerj

Cuestiones relacionadas