NHibernate: Razones para anular Iguales y GetHashCode

¿Hay alguna razón por la cual Equals o GetHashCode se deben anular en las entidades cuando se usa NHibernate? ¿Y en qué escenarios son válidos estos motivos?

Algunas razones que se pueden encontrar en la web:

  • Soporte para carga lenta. La comparación de objetos proxy a través del método Equals predeterminado puede generar errores inesperados. Pero esto debería resolverse mediante un mapa de identidad (y realmente lo es en muchos casos), ¿no es así? Al trabajar con entidades de sesión única, todo debería funcionar bien incluso sin anular Equals / GetHashCode. ¿Hay algún caso en el que el mapa de identidad no funcione bien?
  • Es importante para las colecciones NHibernate. ¿Hay algún caso en que la implementación predeterminada de GetHashCode no sea suficiente (sin incluir los problemas relacionados con Igual)?
  • Mezcla de entidades de varias sesiones y entidades separadas. ¿Es buena idea hacerlo?

¿Alguna otra razón?

Sobrecargar los métodos Equals y GetHashCode es importante si está trabajando con múltiples sesiones, entidades separadas, sesiones sin estado o colecciones (¡mire la respuesta de Sixto Saez para ver un ejemplo!).

En la misma sesión, el mapa de identidad de scope se asegurará de que solo tenga una única instancia de la misma entidad. Sin embargo, existe la posibilidad de comparar una entidad con un proxy de la misma entidad (ver a continuación).

Como mencionas en tu pregunta, la identidad de una instancia de entidad es el principal requisito para anular Equals & GetHashCode . Es una buena práctica en NHibernate usar valores de clave numéricos (cortos, int, o largos según corresponda) porque simplifica el mapeo de una instancia a una fila de base de datos. En el mundo de la base de datos, este valor numérico se convierte en el valor de la columna de la clave principal. Si una tabla tiene lo que se denomina una clave natural (donde varias columnas juntas identifican una fila de forma única), un único valor numérico puede convertirse en una clave primaria sustituta para esta combinación de valores.

Si determina que no desea usar o no puede usar una sola clave principal numérica, necesitará asignar la identidad mediante la funcionalidad NHibernate CompositeKey . En este caso, es absolutamente necesario implementar anulaciones GetHashCode & Equals personalizadas para que la lógica de comprobación del valor de columna para esa tabla pueda determinar la identidad. Aquí hay un buen artículo sobre anular el método GetHashCode y Equals . También debe anular el operador igual para estar completo para todos los usos.

Del comentario: ¿En qué casos es insuficiente la implementación predeterminada de Equals (y GetHashCode )?

La implementación predeterminada no es lo suficientemente buena para NHibernate porque está basada en la implementación Object.Equals . Este método determina la igualdad para los tipos de referencia como igualdad de referencia. En otras palabras, ¿estos dos objetos apuntan a la misma ubicación de memoria? Para NHibernate, la igualdad debe basarse en los valores del mapeo de identidad.

Si no lo hace, lo más probable es que se encuentre con la comparación de un proxy de una entidad con la entidad real y será difícil de depurar . Por ejemplo:

 public class Blog : EntityBase { public virtual string Name { get; set; } // This would be configured to lazy-load. public virtual IList Posts { get; protected set; } public Blog() { Posts = new List(); } public virtual Post AddPost(string title, string body) { var post = new Post() { Title = title, Body = body, Blog = this }; Posts.Add(post); return post; } } public class Post : EntityBase { public virtual string Title { get; set; } public virtual string Body { get; set; } public virtual Blog Blog { get; set; } public virtual bool Remove() { return Blog.Posts.Remove(this); } } void Main(string[] args) { var post = session.Load(postId); // If we didn't override Equals, the comparisons for // "Blog.Posts.Remove(this)" would all fail because of reference equality. // We'd end up be comparing "this" typeof(Post) with a collection of // typeof(PostProxy)! post.Remove(); // If we *didn't* override Equals and *just* did // "post.Blog.Posts.Remove(post)", it'd work because we'd be comparing // typeof(PostProxy) with a collection of typeof(PostProxy) (reference // equality would pass!). } 

Aquí hay una clase base de ejemplo si está usando int como su Id (Que también podría ser abstraído a cualquier tipo de identidad ):

 public abstract class EntityBase where T : EntityBase { public virtual int Id { get; protected set; } protected bool IsTransient { get { return Id == 0; } } public override bool Equals(object obj) { return EntityEquals(obj as EntityBase); } protected bool EntityEquals(EntityBase other) { if (other == null) { return false; } // One entity is transient and the other is not. else if (IsTransient ^ other.IsTransient) { return false; } // Both entities are not saved. else if (IsTransient && other.IsTransient) { return ReferenceEquals(this, other); } else { // Compare transient instances. return Id == other.Id; } } // The hash code is cached because a requirement of a hash code is that // it does not change once calculated. For example, if this entity was // added to a hashed collection when transient and then saved, we need // the same hash code or else it could get lost because it would no // longer live in the same bin. private int? cachedHashCode; public override int GetHashCode() { if (cachedHashCode.HasValue) return cachedHashCode.Value; cachedHashCode = IsTransient ? base.GetHashCode() : Id.GetHashCode(); return cachedHashCode.Value; } // Maintain equality operator semantics for entities. public static bool operator ==(EntityBase x, EntityBase y) { // By default, == and Equals compares references. In order to // maintain these semantics with entities, we need to compare by // identity value. The Equals(x, y) override is used to guard // against null values; it then calls EntityEquals(). return Object.Equals(x, y); } // Maintain inequality operator semantics for entities. public static bool operator !=(EntityBase x, EntityBase y) { return !(x == y); } }