0

What's the proper way to use a composite primary key in NHibernate so it will be amenable for caching?

I isolate the composite primary key similar to the last part of this post: http://devlicio.us/blogs/anne_epstein/archive/2009/11/20/nhibernate-and-composite-keys.aspx

But the second level cache isn't caching it.

If for surrogate key, it is caching, e.g.

 var q = from p in session.Query<Product>()
         select p;

 q.Cacheable().ToList(); // hit database


 // this doesn't hit the database, Get get its value from cache(via last query)
 var px = secondSession.Get<Product>(1); 

But when using composite primary key, the Get doesn't get its value from the cache:

 var q = from pl in session.Query<ProductLanguage>()
         select pl;      

  q.Cacheable().ToList(); // hit the database

  // this hits the database, Get didn't get its value from cache(via last query)
  var plx = secondSession.Get<ProductLanguage>(
         new ProductLanguageCompositeKey { ProductId = 1, LanguageCode = "en" });

Is composite key(ProductLanguageCompositeKey here), even isolated it its own class(with Serializable attribute, Equals and GetHashCode) doesn't get cached?

How can we make an entity accessed via composite key cacheable?

Hao
  • 8,047
  • 18
  • 63
  • 92

2 Answers2

0

For those suspecting if NHibernate's second level cache is not working when using composite primary key (caching works), check if your composite primary key's values are in pristine form. Solution to my caching problem:

SQL Server conversion fidelity from nvarchar to varbinary, then from varbinary to nvarchar

Community
  • 1
  • 1
Hao
  • 8,047
  • 18
  • 63
  • 92
0

For caching a unique cache key is generated for ProductLanguage. This cache key is built from the composite key, which depends on the Product entity's hash code. If you use cross-session queries, NHibernate might return proxied or unproxied versions of Product, which will cause different hash codes and cause the cache lookup to miss the cached ProductLanguage entity.

The solution is to override the Equals and GetHashCode methods in order to return consistent values. The simplest way is to inherit the popular EntityBase class for all entities that have a surrogate Id key.

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

and implemented:

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!).
}

More info at https://stackoverflow.com/a/20110265/179494

Community
  • 1
  • 1
Francois Botha
  • 4,520
  • 1
  • 34
  • 46