Como se menciona en su pregunta, 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 utilizar o no puede utilizar una sola clave primaria numérica, deberá asignar la identidad con NHibernate CompositeKey functionality. En este caso, es absolutamente necesario implementar GetHashCode
& Equals
anulaciones para que la lógica de comprobación del valor de columna para esa tabla pueda determinar la identidad. Here is a good article al 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 el Object.Equals implementation. 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á miserable para depurar.Por ejemplo:
public class Blog : EntityBase<Blog>
{
public virtual string Name { get; set; }
// This would be configured to lazy-load.
public virtual IList<Post> Posts { get; protected set; }
public Blog()
{
Posts = new List<Post>();
}
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<Post>
{
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<Post>(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í es un ejemplo de clase base si está utilizando int
como su Id
(que también podría ser abstraído a any identity type):
public abstract class EntityBase<T>
where T : EntityBase<T>
{
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<T>);
}
protected bool EntityEquals(EntityBase<T> 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<T> x, EntityBase<T> 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<T> x, EntityBase<T> y)
{
return !(x == y);
}
}
Gracias, es un placer escuchar esa respuesta :). ¿Estás seguro de que el mapa de identidad de sesión no está roto en algunos casos de esquina? ¿Tiene algún ejemplo cuando trabajar con varias sesiones es inevitable? –
Nunca debe obtener 2 objetos diferentes que representen esa misma entidad (como en el mismo tipo de clase y la misma fila de la base de datos) en la misma sesión. De lo contrario, sería un error en NHibernate. En ocasiones, puede utilizar una sesión sin estado (independientemente de la sesión principal) para realizar operaciones de inserción/actualización por lotes o escribir datos de auditoría en la base de datos. Por ejemplo, no puede usar su sesión principal dentro de los interceptores de persistencia. –
@DmitryS. * puede * obtener dos objetos que representan la misma entidad: una entidad proxy en una entidad de colección y no proxy. Si el no proxy hace referencia a la entidad de recopilación, la igualdad de referencia (la predeterminada en NHibernate) ** falla **. Por lo tanto, siempre se recomienda anular iguales. – TheCloudlessSky