[java] El dilema de hashCode () / equals () de JPA


Answers

Siempre anulo equals / hashcode y lo implemento en función de la identificación comercial. 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 con las diferentes formas de manejar equals / hashCode:

EDITAR :

Para explicar por qué esto funciona para mí:

  1. Normalmente no utilizo la recolecció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 id empresarial en el tiempo de ejecución no es una mejor práctica para cualquier aplicación de base de datos. En casos raros donde no hay otra solución, haría un tratamiento especial como eliminar el elemento y volver a ponerlo en la colección basada en hash.
  3. Para mi modelo, establecí el id empresarial en el constructor y no proporciono setters para él. Permití que la implementación de JPA cambiara el campo en lugar de la propiedad.
  4. La solución UUID parece ser excesiva. ¿Por qué UUID si tiene una identificación empresarial natural? Después de todo, establecería la singularidad de la identificación comercial en la base de datos. ¿Por qué entonces tener TRES índices para cada tabla en la base de datos?
Question

Hubo some discussions aquí sobre las entidades JPA y qué hashCode() / equals() debería usarse para las clases de entidad JPA. La mayoría (si no todos) de ellos dependen de Hibernate, pero me gustaría hablar sobre su implementación de JPA de forma neutral (estoy usando EclipseLink, por cierto).

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

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

Hasta donde puedo ver, hay tres opciones :

  1. No los anules; confía en Object.equals() y Object.hashCode()
    • hashCode() / equals() trabajo
    • no puede identificar objetos idénticos, problemas con proxies dinámicos
    • sin problemas con entidades separadas
  2. Anularlos, basado en la clave primaria
    • hashCode() / equals() están rotos
    • identidad correcta (para todas las entidades administradas)
    • problemas con entidades separadas
  3. Anularlos, basado en el Business-Id (campos clave no primarios, ¿qué pasa con las claves externas?)
    • hashCode() / equals() están rotos
    • identidad correcta (para todas las entidades administradas)
    • sin problemas con entidades separadas

Mis preguntas son:

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



ACTUALIZACIÓN 1:

Si " hashCode() / equals() están rotos", quiero decir que las sucesivas invocaciones de hashCode() pueden devolver diferentes valores, que (cuando se implementan correctamente) no se rompen en el sentido de la documentación de Object API, pero que causan 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é enfoque será el mejor para una aplicación de la vida real, o cómo determinar el mejor enfoque para mi aplicación. Por lo tanto, voy a mantener abierta la pregunta y espero tener más discusiones y / u opiniones.




Please consider the following approach based on predefined type identifier and the ID.

The specific assumptions for JPA:

  • entities of the same "type" and the same non-null ID are considered equal
  • non-persisted entities (assuming no ID) are never equal to other entities

The abstract entity:

@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;
  }
}

Concrete entity example:

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

Test example:

@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());
}

Main advantages here:

  • simplicity
  • ensures subclasses provide type identity
  • predicted behavior with proxied classes

Disadvantages:

  • Requires each entity to call super()

Notas:

  • Needs attention when using inheritance. Eg instance equality of class A and class B extends A may depend on concrete details of the application.
  • Ideally, use a business key as the ID

Looking forward to your comments.




Aunque el uso de una clave de negocios (opción 3) es el enfoque más recomendado ( wiki de la comunidad de Hibernate , "Persistencia de Java con Hibernate", página 398), y esto es lo que más utilizamos, hay un error de Hibernate que rompe esto por ansioso juegos: 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 recomendado de clave empresarial sea problemático.

Creo que el meollo de la cuestión es que equals y hashCode deben basarse en el estado inmutable (referencia Odersky et al. ), Y una entidad Hibernate con clave principal gestionada por Hibernate no tiene tal estado inmutable. La clave primaria 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, heredando las implementaciones java.lang.Object basadas en la identidad del objeto, o usando una clave primaria administrada por la aplicación como sugiere James Brundege en "Do not Let Hibernate Steal Your Identity" (ya mencionado por la respuesta de Stijn Geukens ) y por Lance Arlaus en "Object Generation: A Better Approach to Hibernate Integration" .

El mayor problema con la opción 1 es que las instancias separadas no se pueden comparar con las instancias persistentes usando .equals (). Pero eso esta bien; el contrato de iguales y hashCode le deja al desarrollador decidir qué significa igualdad para cada clase. Así que simplemente deje 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 tal fin, tal vez boolean sameEntity o boolean dbEquivalent o boolean businessEquals .




Below is a simple (and tested) solution for Scala.

  • Note that this solution does not fit into any of the 3 categories given in the question.

  • All my Entities are subclasses of the UUIDEntity so I follow the don't-repeat-yourself (DRY) principle.

  • Si es necesario, la generación de UUID puede hacerse más precisa (mediante el uso de números más pseudoaleatorios).

Código de Scala:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}



There are obviously already very informative answers here but I will tell you what we do.

We do nothing (ie do not override).

If we do need equals/hashcode to work for collections we use UUIDs. You just create the UUID in the constructor. We use http://wiki.fasterxml.com/JugHome for UUID. UUID is a little more expensive CPU wise but is cheap compared to serialization and db access.




I tried to answer this question myself and was never totally happy with found solutions until i read this post and especially DREW one. I liked the way he lazy created UUID and optimally stored it.

But I wanted to add even more flexibility, ie lazy create UUID ONLY when hashCode()/equals() is accessed before first persistence of the entity with each solution's advantages :

  • equals() means "object refers to the same logical entity"
  • use database ID as much as possible because why would I do the work twice (performance concern)
  • prevent problem while accessing hashCode()/equals() on not yet persisted entity and keep the same behaviour after it is indeed persisted

I would really apreciate feedback on my mixed-solution below

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }



I have always used option 1 in the past because I was aware of these discussions and thought it was better to do nothing until I knew the right thing to do. Those systems are all still running successfully.

However, next time I may try option 2 - using the database generated Id.

Hashcode and equals will throw IllegalStateException if the id is not set.

This will prevent subtle errors involving unsaved entities from appearing unexpectedly.

What do people think of this approach?




Personalmente ya usé todas estas tres estrategias en diferentes proyectos. Y debo decir que la opción 1 es, en mi opinión, la más practicable en una aplicación de la vida real. A hizo que la experiencia de romper la conformidad de hashCode () / equals () condujera a muchos errores locos, ya que cada vez terminarás en situaciones en las que el resultado de la igualdad cambia después de que una entidad se haya agregado a una colección.

Pero hay más opciones (también con sus pros y sus contras):

a) hashCode / equals basado en un conjunto de campos inmutables , no nulos , asignados por el constructor

(+) los tres criterios están garantizados

(-) los valores de campo deben estar disponibles para crear una nueva instancia

(-) complicar el manejo si debe cambiar uno de los siguientes

b) hashCode / equals basado en la clave primaria asignada por la aplicación (en constructor) en lugar de JPA

(+) los tres criterios están garantizados

(-) no puede aprovechar las estrategias de generación de ID simples y fiables, como las secuencias de DB

(-) complicado si se crean nuevas entidades en un entorno distribuido (cliente / servidor) o clúster de servidores de aplicaciones

c) hashCode / equals basado en un UUID asignado por el constructor de la entidad

(+) los tres criterios están garantizados

(-) sobrecarga de la generación de UUID

(-) puede existir un pequeño riesgo de que se use el doble del mismo UUID, dependiendo del algoritmo utilizado (puede detectarse mediante un índice único en DB)




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 considerar la creación lenta del 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();
    }

}



Related