The solution I propose involves quite a bit of code, but you can just copy it all and past it in a VS test solution assuming you have SqLite installed, and you should be able to run the tests yourself.
As I have been struggling with the object identity versus object equality and database identity problem using Nhibernate, I have read various posts. However, I could not get a clear picture of how to properly set up object identity in conjunction with collections. Basically, the big problem, as I got, is that once an object is added to a collection it's identity (as derived by the GetHashCode) method cannot change. The preferred way to implement the GetHasHCode is to use a business key. But what if the business key was not the right one? I would like to have that entity updated with it's new business key. But then my collections are out of sync as I violated the immutability of the identity of that object.
The below code is a proposal to solve this problem. However, as I am certainly not a NHibernate expert and also not a very experienced developer, I would gladly receive comments from more senior developers whether this is a viable approach.
using System;
using System.Collections.Generic;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Mapping;
using Iesi.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
using NHibernate.Util;
namespace NHibernateTests
{
public class InMemoryDatabase : IDisposable
{
private static Configuration _configuration;
private static ISessionFactory _sessionFactory;
private ISession _session;
public ISession Session { get { return _session ?? (_session = _sessionFactory.OpenSession()); } }
public InMemoryDatabase()
{
// Uncomment this line if you do not use NHProfiler
HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize();
_sessionFactory = CreateSessionFactory();
BuildSchema(Session);
}
private static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory().Raw("hbm2ddl.keywords", "none").ShowSql())
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<Brand>())
.ExposeConfiguration(cfg => _configuration = cfg)
.BuildSessionFactory();
}
private static void BuildSchema(ISession Session)
{
SchemaExport export = new SchemaExport(_configuration);
export.Execute(true, true, false, Session.Connection, null);
}
public void Dispose()
{
Session.Dispose();
}
}
public abstract class Entity<T>
where T: Entity<T>
{
private readonly IEqualityComparer<T> _comparer;
protected Entity(IEqualityComparer<T> comparer)
{
_comparer = comparer;
}
public virtual Guid Id { get; protected set; }
public virtual bool IsTransient()
{
return Id == Guid.Empty;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
return _comparer.Equals((T)this, (T)obj);
}
public override int GetHashCode()
{
return _comparer.GetHashCode((T)this);
}
}
public class Brand: Entity<Brand>
{
protected Brand() : base(new BrandComparer()) {}
public Brand(String name) : base (new BrandComparer())
{
SetName(name);
}
private void SetName(string name)
{
Name = name;
}
public virtual String Name { get; protected set; }
public virtual Manufactor Manufactor { get; set; }
public virtual void ChangeName(string name)
{
Name = name;
}
}
public class BrandComparer : IEqualityComparer<Brand>
{
public bool Equals(Brand x, Brand y)
{
return x.Name == y.Name;
}
public int GetHashCode(Brand obj)
{
return obj.Name.GetHashCode();
}
}
public class BrandMap : ClassMap<Brand>
{
public BrandMap()
{
Id(x => x.Id).GeneratedBy.GuidComb();
Map(x => x.Name).Not.Nullable().Unique();
References(x => x.Manufactor)
.Cascade.SaveUpdate();
}
}
public class Manufactor : Entity<Manufactor>
{
private Iesi.Collections.Generic.ISet<Brand> _brands = new HashedSet<Brand>();
protected Manufactor() : base(new ManufactorComparer()) {}
public Manufactor(String name) : base(new ManufactorComparer())
{
SetName(name);
}
private void SetName(string name)
{
Name = name;
}
public virtual String Name { get; protected set; }
public virtual Iesi.Collections.Generic.ISet<Brand> Brands
{
get { return _brands; }
protected set { _brands = value; }
}
public virtual void AddBrand(Brand brand)
{
if (_brands.Contains(brand)) return;
_brands.Add(brand);
brand.Manufactor = this;
}
}
public class ManufactorMap : ClassMap<Manufactor>
{
public ManufactorMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasMany(x => x.Brands)
.AsSet()
.Cascade.AllDeleteOrphan().Inverse();
}
}
public class ManufactorComparer : IEqualityComparer<Manufactor>
{
public bool Equals(Manufactor x, Manufactor y)
{
return x.Name == y.Name;
}
public int GetHashCode(Manufactor obj)
{
return obj.Name.GetHashCode();
}
}
public static class IdentityChanger
{
public static void ChangeIdentity<T>(Action<T> changeIdentity, T newIdentity, ISession session)
{
changeIdentity.Invoke(newIdentity);
session.Flush();
session.Clear();
}
}
[TestClass]
public class BusinessIdentityTest
{
private InMemoryDatabase _db;
[TestInitialize]
public void SetUpInMemoryDb()
{
_db = new InMemoryDatabase();
}
[TestCleanup]
public void DisposeInMemoryDb()
{
_db.Dispose();
}
[TestMethod]
public void ThatBrandIsIdentifiedByBrandComparer()
{
var brand = new Brand("Dynatra");
Assert.AreEqual("Dynatra".GetHashCode(), new BrandComparer().GetHashCode(brand));
}
[TestMethod]
public void ThatSetOfBrandIsHashedByBrandComparer()
{
var brand = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
Assert.IsTrue(manufactor.Brands.Contains(brand));
}
[TestMethod]
public void ThatHashOfBrandInSetIsThatOfComparer()
{
var brand = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
Assert.AreEqual(manufactor.Brands.First().GetHashCode(), "Dynatra".GetHashCode());
}
[TestMethod]
public void ThatSameBrandCannotBeAddedTwice()
{
var brand = new Brand("Dynatra");
var duplicate = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
manufactor.AddBrand(duplicate);
Assert.AreEqual(1, manufactor.Brands.Count);
}
[TestMethod]
public void ThatPersistedBrandIsSameAsLoadedBrandWithSameId()
{
var brand = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
_db.Session.Transaction.Begin();
_db.Session.Save(brand);
var copy = _db.Session.Load<Brand>(brand.Id);
_db.Session.Transaction.Commit();
Assert.AreSame(brand, copy);
}
[TestMethod]
public void ThatLoadedBrandIsContainedByManufactor()
{
var brand = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
_db.Session.Transaction.Begin();
_db.Session.Save(brand);
var copy = _db.Session.Load<Brand>(brand.Id);
_db.Session.Transaction.Commit();
Assert.IsTrue(brand.Manufactor.Brands.Contains(copy));
}
[TestMethod]
public void ThatAbrandThatIsLoadedUsesTheSameHash()
{
var brand = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
_db.Session.Transaction.Begin();
_db.Session.Save(brand);
var id = brand.Id;
brand = _db.Session.Load<Brand>(brand.Id);
Assert.IsTrue(brand.Manufactor.Brands.Contains(new Brand("Dynatra")));
}
[TestMethod]
public void ThatBrandCannotBeFoundIfIdentityChanges()
{
var brand = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
_db.Session.Transaction.Begin();
_db.Session.Save(brand);
Assert.IsTrue(brand.Manufactor.Brands.Contains(new Brand("Dynatra")));
brand.ChangeName("Dynatra_");
Assert.AreEqual("Dynatra_", brand.Name);
Assert.AreEqual("Dynatra_".GetHashCode(), brand.Manufactor.Brands.First().GetHashCode());
Assert.IsFalse(brand.Manufactor.Brands.Contains(brand));
// ToDo: I don't understand why this test fails
Assert.IsTrue(brand.Manufactor.Brands.Contains(new Brand("Dynatra")));
}
[TestMethod]
public void ThatSessionNeedsToBeClearedAfterIdentityChange()
{
var brand = new Brand("Dynatra");
var manufactor = new Manufactor("Lily");
manufactor.AddBrand(brand);
_db.Session.Transaction.Begin();
_db.Session.Save(brand);
var id = brand.Id;
brand = _db.Session.Load<Brand>(brand.Id);
// This makes the test pass
IdentityChanger.ChangeIdentity(brand.ChangeName, "Dynatra_", _db.Session);
brand = _db.Session.Load<Brand>(id);
Assert.IsFalse(brand.Manufactor.Brands.Contains(new Brand("Dynatra")));
Assert.IsTrue(brand.Manufactor.Brands.Contains(new Brand("Dynatra_")));
}
}
}
Important edit! I now consider the approach I was suggesting, as has been pointed out as not the right approach. I have provided a different answer to the dilemma I was facing.
I think the basic misconception here is that you implement Equals and GetHashCode based on business data. I don't know why you prefer that, I can't see any advantage in it. Except - of course - when dealing with a value object which doesn't have an Id.
There is a great post on nhforge.org about Identity Field, Equality and Hash Code
Edit: This part of your code will cause problems:
public static class IdentityChanger
{
public static void ChangeIdentity<T>(Action<T> changeIdentity, T newIdentity, ISession session)
{
changeIdentity.Invoke(newIdentity);
session.Flush();
session.Clear();
}
}
You should implement Equals
and GetHashCode
based on immutable data. Changing the hash is not possible in a reasonable way.