The question is: why do you want to do this? I think it is not much of an optimization if you only update columns that have changed.
Have you got triggers in the database?
If so, you can do the following:
- use
select-before-update="true"
and dynamic-update="true"
in the mapping. This makes NH to perform a query before the update and only updates if it changed, and only the columns that have changed. I'm not sure if it selects for every update, or only if it is not in the session.
- use
Merge
instead of update. This does actually the same: it selects the entity from the database, only if it is not already in the session. Also use dynamic-update="true"
. There is also a trade-off: Merge
returns the attached instance if there is already one in the session. So you should always throw away the instance you passed in and work with the instance you go from Merge
.
Actually I wouldn't care about the updated columns. Its most probably faster to update them blindly instead of performing a preceding query.
As you mention in your question, identity of an entity instance is the main requirement for overriding Equals
& GetHashCode
. It is a best practice in NHibernate to use numeric key values (short, int, or long as appropriate) because it simplifies mapping an instance to a database row. In the database world, this numeric value becomes the primary key column value. If a table has what is called a natural key (where several columns together uniquely identify a row) then a single numeric value can become a surrogate primary key for this combination of values.
If you determine that you don't want to use or are prevented from using a single numeric primary key then you'll need to map the identity using the NHibernate CompositeKey functionality. In this case, you absolutely need to implement custom GetHashCode
& Equals
overrides so the column value checking logic for that table can determine identity. Here is a good article on overriding the GetHashCode
and Equals
method. You should also override the equal operator to be complete for all usages.
From the comment: In which cases is default implementation of Equals
(and GetHashCode
) insufficient?
The default implementation is not good enough for NHibernate because it is based on the Object.Equals implementation. This method determines equality for reference types as reference equality. In other words, are these two objects pointing to the same memory location? For NHibernate, the equality should be based on the value(s) of the identity mapping.
If you don't, you will most likely run into comparing a proxy of an entity with the real entity and it will be miserable to debug. For example:
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!).
}
Here is an example base class if you're using int
as your Id
(which could also be abstracted to 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);
}
}
Best Answer
We tried this in a client-server architecture. Now we are moving to DTO (data transfer objects). This means, the detached entities are not directly sent to the client anymore, but specialized objects.
The main reason to move in this direction is not NHibernate, it is actually the serialization needed to send entities to the client. While you could use lazy-loading (and you will!) while you are attached to the session, you need to get all references from the database to serialize it.
We had lots of Guids instead of references and lots of properties which are mapped but not serialized ... and it became a pain. So it's much easier to copy the stuff you really want to serialize to its own structure.
Besides of that - working detached could work well.