java example ejemplo - El JPA hashCode () / equals () dilema





9 Answers

Siempre sobrescribo equals / hashcode y lo implemento en función del ID de empresa. Parece la solución más razonable para mí. Vea el siguiente link .

Para resumir todo esto, aquí hay una lista de lo que funcionará o no funcionará con las diferentes formas de manejar equals / hashCode:

EDITAR :

Para explicar por qué esto funciona para mí:

  1. Normalmente no uso la colección basada en hash (HashMap / HashSet) en mi aplicación JPA. Si debo hacerlo, prefiero crear la solución UniqueList.
  2. Creo que cambiar la identificación empresarial en tiempo de ejecución no es una buena práctica para ninguna aplicación de base de datos. En los casos excepcionales en los que no hay otra solución, haría un tratamiento especial como eliminar el elemento y volver a colocarlo en la colección basada en hash.
  3. Para mi modelo, establezco el ID de negocio en el constructor y no proporciono configuradores para él. Dejo la implementación de JPA para cambiar el campo en lugar de la propiedad.
  4. La solución UUID parece ser una exageración. ¿Por qué UUID si tienes identificación de negocio natural? Después de todo, establecería la unicidad del ID de empresa en la base de datos. ¿Por qué tener TRES índices para cada tabla en la base de datos entonces?
when override and

Ha habido some discussions aquí sobre las entidades JPA y qué hashCode() / equals() debe usar para las clases de entidades JPA. La mayoría (si no todos) de ellos dependen de Hibernate, pero me gustaría discutirlos de forma neutra con JPA (estoy utilizando EclipseLink, por cierto).

Todas las implementaciones posibles tienen sus propias ventajas y desventajas con respecto a:

  • hashCode() / equals() conformidad de contrato (inmutabilidad) para operaciones de List / Set
  • Se pueden detectar objetos idénticos (por ejemplo, de diferentes sesiones, proxies dinámicos de estructuras de datos cargadas perezosamente)
  • Si las entidades se comportan correctamente en estado separado (o no persistente)

Por lo que puedo ver, hay tres opciones :

  1. No los anules; confiar en Object.equals() y Object.hashCode()
    • hashCode() / equals() trabajo
    • no se pueden identificar objetos idénticos, problemas con proxies dinámicos
    • no hay problemas con las entidades separadas
  2. Anularlos, según la clave principal
    • hashCode() / equals() están rotos
    • Identidad correcta (para todas las entidades gestionadas)
    • problemas con entidades separadas
  3. Anularlos, en función de la identificación de la empresa (campos de clave no principal; ¿qué pasa con las claves externas?)
    • hashCode() / equals() están rotos
    • Identidad correcta (para todas las entidades gestionadas)
    • no hay problemas con las entidades separadas

Mis preguntas son:

  1. ¿Me perdí una opción y / o un punto pro / con?
  2. ¿Qué opción elegiste y por qué?



ACTUALIZACIÓN 1:

Por " hashCode() / equals() se rompe", me refiero a que las sucesivas invocaciones de hashCode() pueden devolver valores diferentes, lo que no se rompe (cuando se implementa correctamente) en el sentido de la documentación de la API de Object , pero causa problemas al intentar para recuperar una entidad modificada de un Map , Set u otra Collection basada en hash. En consecuencia, las implementaciones de JPA (al menos EclipseLink) no funcionarán correctamente en algunos casos.

ACTUALIZACIÓN 2:

Gracias por sus respuestas, la mayoría de ellas tienen una calidad notable.
Lamentablemente, todavía no estoy seguro de qué método será el mejor para una aplicación de la vida real o cómo determinar cuál es el mejor método para mi aplicación. Por lo tanto, mantendré la pregunta abierta y esperaré más discusiones u opiniones.




Si desea usar equals()/hashCode() para sus Conjuntos, en el sentido de que la misma entidad solo puede estar allí una vez, entonces solo hay una opción: Opción 2. Eso es una clave primaria para una entidad por definición nunca cambia (si alguien lo actualiza, ya no es la misma entidad)

Debe tomar eso literalmente: ya que su equals()/hashCode() se basa en la clave principal, no debe usar estos métodos hasta que se establezca la clave principal. Así que no deberías poner entidades en el conjunto, hasta que se les asigne una clave primaria. (Sí, los UUID y conceptos similares pueden ayudar a asignar claves primarias antes de tiempo).

Ahora, en teoría, también es posible lograrlo con la Opción 3, aunque las llamadas "claves de negocios" tienen el inconveniente desagradable de que pueden cambiar: "Todo lo que tendrá que hacer es eliminar las entidades ya insertadas del conjunto ( s), y reinsertarlos. Eso es cierto, pero también significa que, en un sistema distribuido, tendrá que asegurarse de que esto se haga absolutamente en cualquier lugar donde se hayan insertado los datos (y tendrá que asegurarse de que se realiza la actualización). , antes de que ocurran otras cosas). Necesitará un mecanismo de actualización sofisticado, especialmente si algunos sistemas remotos no son accesibles actualmente ...

La opción 1 solo se puede usar, si todos los objetos en sus conjuntos son de la misma sesión de Hibernate. La documentación de Hibernate lo deja muy claro en el capítulo 13.1.3. Teniendo en cuenta la identidad del objeto :

Dentro de una sesión, la aplicación puede usar con seguridad == para comparar objetos.

Sin embargo, una aplicación que utiliza == fuera de una sesión puede producir resultados inesperados. Esto puede ocurrir incluso en algunos lugares inesperados. Por ejemplo, si coloca dos instancias separadas en el mismo Conjunto, ambas podrían tener la misma identidad de base de datos (es decir, representan la misma fila). Sin embargo, la identidad de JVM no está garantizada, por definición, para instancias en un estado separado. El desarrollador debe anular los métodos equals () y hashCode () en clases persistentes e implementar su propia noción de igualdad de objetos.

Continúa argumentando a favor de la Opción 3:

Hay una advertencia: nunca use el identificador de base de datos para implementar la igualdad. Utilice una clave de negocio que sea una combinación de atributos únicos, generalmente inmutables. El identificador de la base de datos cambiará si un objeto transitorio se hace persistente. Si la instancia transitoria (generalmente junto con las instancias separadas) se mantiene en un Conjunto, el cambio del hashcode rompe el contrato del Conjunto.

Esto es verdad, si usted

  • no se puede asignar la identificación antes (por ejemplo, mediante el uso de UUID)
  • y sin embargo, absolutamente desea poner sus objetos en conjuntos mientras están en estado transitorio.

De lo contrario, eres libre de elegir la Opción 2.

Luego menciona la necesidad de una estabilidad relativa:

Los atributos para las claves de negocios no tienen que ser tan estables como las claves primarias de la base de datos; solo tiene que garantizar la estabilidad siempre que los objetos estén en el mismo Conjunto.

Esto es correcto. El problema práctico que veo con esto es: si no puede garantizar la estabilidad absoluta, ¿cómo podrá garantizar la estabilidad "siempre que los objetos estén en el mismo Conjunto"? Puedo imaginar algunos casos especiales (como usar conjuntos solo para una conversación y luego desecharlos), pero cuestionaría la posibilidad general de esto.

Version corta:

  • La opción 1 solo se puede utilizar con objetos dentro de una sola sesión.
  • Si puede, use la Opción 2. (Asigne PK lo antes posible, porque no puede usar los objetos en conjuntos hasta que se asigne la PK).
  • Si puede garantizar una estabilidad relativa, puede usar la Opción 3. Pero tenga cuidado con esto.



Aunque el uso de una clave de negocio (opción 3) es el método más comúnmente recomendado ( wiki de la comunidad de Hibernate , "Persistencia de Java con Hibernate", pág. 398), y esto es lo que usamos principalmente, hay un error de Hibernate que lo rompe por algo que buscamos. Conjuntos: HHH-3799 . En este caso, Hibernate puede agregar una entidad a un conjunto antes de que se inicialicen sus campos. No estoy seguro de por qué este error no ha recibido más atención, ya que realmente hace que el enfoque de clave de negocios recomendado sea problemático.

Creo que el meollo de la cuestión es que equals y hashCode deben basarse en un estado inmutable (referencia Odersky et al. ), Y una entidad de Hibernate con clave primaria administrada por Hibernate no tiene tal estado inmutable. La clave principal es modificada por Hibernate cuando un objeto transitorio se vuelve persistente. La clave de negocio también es modificada por Hibernate, cuando hidrata un objeto en el proceso de inicialización.

Eso deja solo la opción 1, heredar las implementaciones java.lang.Object basadas en la identidad del objeto, o usar una clave primaria administrada por la aplicación como lo sugiere James Brundege en "No dejes que Hibernate robe tu identidad" (ya referenciada por la respuesta de Stijn Geukens ) y por Lance Arlaus en "Generación de objetos: un mejor enfoque para la integración de hibernación" .

El mayor problema con la opción 1 es que las instancias separadas no pueden compararse con las instancias persistentes que usan .equals (). Pero eso esta bien; El contrato de equals y hashCode lo deja al desarrollador para decidir qué significa la igualdad para cada clase. Así que simplemente deja que equals y hashCode hereden de Object. Si necesita comparar una instancia separada con una instancia persistente, puede crear un nuevo método explícitamente para ese propósito, tal vez boolean sameEntity o boolean dbEquivalent o boolean businessEquals .




Estoy de acuerdo con la respuesta de Andrew. Hacemos lo mismo en nuestra aplicación, pero en lugar de almacenar UUID como VARCHAR / CHAR, lo dividimos en dos valores largos. Consulte UUID.getLeastSignificantBits () y UUID.getMostSignificantBits ().

Una cosa más a tener en cuenta es que las llamadas a UUID.randomUUID () son bastante lentas, por lo que es posible que desee ver la generación perezosa de UUID solo cuando sea necesario, como durante la persistencia o llamadas a equals () / hashCode ()

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}



Obviamente ya hay respuestas muy informativas aquí, pero les diré lo que hacemos.

No hacemos nada (es decir, no anular).

Si necesitamos iguales / hashcode para trabajar con las colecciones, usamos UUID. Solo creas el UUID en el constructor. Usamos http://wiki.fasterxml.com/JugHome para UUID. UUID es un CPU un poco más caro, pero es barato en comparación con la serialización y el acceso a base de datos.




Considere el siguiente enfoque basado en el identificador de tipo predefinido y el ID.

Los supuestos específicos para JPA:

  • Las entidades del mismo "tipo" y la misma ID no nula se consideran iguales
  • las entidades no persistentes (suponiendo que no hay ID) nunca son iguales a otras entidades

La entidad abstracta:

@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

  public AbstractPersistable(final String kind) {
    this.kind = requireNonNull(kind, "Entity kind cannot be null");
  }

  @Override
  public final boolean equals(final Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof AbstractPersistable)) return false;
    final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null != this.id
        && Objects.equals(this.id, that.id)
        && Objects.equals(this.kind, that.kind);
  }

  @Override
  public final int hashCode() {
    return Objects.hash(kind, id);
  }

  public K getId() {
    return id;
  }

  protected void setId(final K id) {
    this.id = id;
  }
}

Ejemplo de entidad concreta:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

Ejemplo de prueba:

@Test
public void test_EqualsAndHashcode_GivenSubclass() {
  // Check contract
  EqualsVerifier.forClass(Foo.class)
    .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
    .withOnlyTheseFields("id", "kind")
    .withNonnullFields("id", "kind")
    .verify();
  // Ensure new objects are not equal
  assertNotEquals(new Foo(), new Foo());
}

Principales ventajas aquí:

  • sencillez
  • asegura que las subclases proporcionan la identidad de tipo
  • comportamiento predicho con clases de proxy

Desventajas:

  • Requiere que cada entidad llame super()

Notas:

  • Necesita atención al usar la herencia. Por ejemplo, la igualdad de class Ay class B extends Apuede depender de detalles concretos de la aplicación.
  • Lo ideal es utilizar una clave de negocio como la ID

Esperamos sus comentarios.




Este es un problema común en todos los sistemas de TI que usan Java y JPA. El punto de dolor se extiende más allá de la implementación de equals () y hashCode (), afecta a cómo una organización se refiere a una entidad y cómo sus clientes se refieren a la misma entidad. He visto suficiente dolor por no tener una clave de negocios hasta el punto de que escribí mi propio blog para expresar mi opinión.

En resumen: use una ID secuencial, corta y legible por humanos, con prefijos significativos como clave de negocio que se genere sin ninguna dependencia de ningún almacenamiento que no sea RAM. El Snowflake de Snowflake de Twitter es un muy buen ejemplo.




Si UUID es la respuesta para muchas personas, ¿por qué no solo usamos métodos de fábrica de la capa empresarial para crear las entidades y asignar la clave principal en el momento de la creación?

por ejemplo:

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

de esta manera obtendríamos una clave primaria predeterminada para la entidad del proveedor de persistencia, y nuestras funciones hashCode () y equals () podrían basarse en eso.

También podríamos declarar protegidos a los constructores del Coche y luego usar la reflexión en nuestro método comercial para acceder a ellos. De esta manera, los desarrolladores no tienen la intención de crear una instancia de Car con el nuevo, sino a través del método de fábrica.

¿Qué tal eso?




En la práctica, parece que la Opción 2 (Clave principal) se usa con más frecuencia. Las claves de negocio naturales e INMUTABLES son pocas veces, la creación y el soporte de claves sintéticas son demasiado pesadas para resolver situaciones, que probablemente nunca ocurran. Eche un vistazo a la implementación Abstract-Persistable de spring-data-jpa (lo único: para el uso de la implementación de HibernateHibernate.getClass ).

 public boolean equals(Object obj) { if (null == obj) { return false; } if (this == obj) { return true; } if (!getClass().equals(ClassUtils.getUserClass(obj))) { return false; } AbstractPersistable<?> that = (AbstractPersistable<?>) obj; return null == this.getId() ? false : this.getId().equals(that.getId()); } @Override public int hashCode() { int hashCode = 17; hashCode += null == getId() ? 0 : getId().hashCode() * 31; return hashCode; } 

Solo consciente de manipular objetos nuevos en HashSet / HashMap. En el opuesto, la Opción 1 (permanecer en la Objectimplementación) se rompe justo después merge, esa es una situación muy común.

Si no tiene una clave comercial y tiene una REAL necesidad de manipular una nueva entidad en la estructura hash, anule hashCodea constante, como se indicó a continuación Vlad Mihalcea.




Related