我提出的解决方案涉及相当多的代码,但您可以将其全部复制并在VS测试解决方案中将其复制,假设您已安装SqLite,并且您应该能够自己运行测试。
由于我一直在努力使用Nhibernate来对象身份与对象相等和数据库身份问题,我已经阅读了各种帖子。但是,我无法清楚地了解如何与集合一起正确设置对象标识。基本上,我得到的一个大问题是,一旦将一个对象添加到一个集合中,它的身份(由GetHashCode派生)方法就无法改变。实现GetHasHCode的首选方法是使用业务密钥。但是,如果业务密钥不正确呢?我希望用它的新业务密钥更新该实体。但是后来我的收藏品不同步,因为我违反了该对象身份的不变性。
以下代码是解决此问题的提案。但是,由于我当然不是NHibernate专家,也不是非常有经验的开发人员,我很乐意收到更多高级开发人员的评论,这是否是一种可行的方法。
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_")));
}
}
}
重要编辑!我现在考虑我所建议的方法,正如已经指出的那样,这不是正确的方法。我为我所面临的两难困境提供了不同的答案。
答案 0 :(得分:3)
这是一个有趣的方法,但我不会花时间去理解和批评,而是只提供解决这个问题的方法。
我不喜欢通用实体基类的想法,所以我的解决方案只支持int,Guid和字符串标识。下面的一些代码(例如使用Func<int>
来获取哈希代码)仅存在以支持不区分大小写的字符串比较。如果我忽略了字符串标识符(我希望我能),代码会更紧凑。
这段代码通过了我的单元测试,并没有让我失望,但我确信有边缘情况。我唯一想到的是:如果我新建并保存实体,它将保留其原始哈希码,但如果在保存之后我从另一个会话中的数据库中检索同一实体的实例,它将具有不同的哈希代码。
欢迎反馈。
基类:
[Serializable]
public abstract class Entity
{
protected int? _cachedHashCode;
public abstract bool IsTransient { get; }
// Check equality by comparing transient state or id.
protected bool EntityEquals(Entity other, Func<bool> idEquals)
{
if (other == null)
{
return false;
}
if (IsTransient ^ other.IsTransient)
{
return false;
}
if (IsTransient && other.IsTransient)
{
return ReferenceEquals(this, other);
}
return idEquals.Invoke();
}
// Use cached hash code to ensure that hash code does not change when id is assigned.
protected int GetHashCode(Func<int> idHashCode)
{
if (!_cachedHashCode.HasValue)
{
_cachedHashCode = IsTransient ? base.GetHashCode() : idHashCode.Invoke();
}
return _cachedHashCode.Value;
}
}
int identity:
[Serializable]
public abstract class EntityIdentifiedByInt : Entity
{
public abstract int Id { get; }
public override bool IsTransient
{
get { return Id == 0; }
}
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (EntityIdentifiedByInt)obj;
return Equals(other);
}
public virtual bool Equals(EntityIdentifiedByInt other)
{
return EntityEquals(other, () => Id == other.Id);
}
public override int GetHashCode()
{
return GetHashCode(() => Id);
}
}
Guid身份:
[Serializable]
public abstract class EntityIdentifiedByGuid : Entity
{
public abstract Guid Id { get; }
public override bool IsTransient
{
get { return Id == Guid.Empty; }
}
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (EntityIdentifiedByGuid)obj;
return Equals(other);
}
public virtual bool Equals(EntityIdentifiedByGuid other)
{
return EntityEquals(other, () => Id == other.Id);
}
public override int GetHashCode()
{
return GetHashCode(() => Id.GetHashCode());
}
}
字符串标识:
[Serializable]
public abstract class EntityIdentifiedByString : Entity
{
public abstract string Id { get; }
public override bool IsTransient
{
get { return Id == null; }
}
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (EntityIdentifiedByString)obj;
return Equals(other);
}
public virtual bool Equals(EntityIdentifiedByString other)
{
Func<bool> idEquals = () => string.Equals(Id, other.Id, StringComparison.OrdinalIgnoreCase);
return EntityEquals(other, idEquals);
}
public override int GetHashCode()
{
return GetHashCode(() => Id.ToUpperInvariant().GetHashCode());
}
}
答案 1 :(得分:1)
我认为这里的基本误解是你根据业务数据实现了Equals和GetHashCode。我不知道你为什么喜欢它,我看不出它有什么优势。当然 - 当处理没有Id的值对象时。
nhforge.org上关于Identity Field, Equality and Hash Code
的帖子很棒修改:这部分代码会导致问题:
public static class IdentityChanger
{
public static void ChangeIdentity<T>(Action<T> changeIdentity, T newIdentity, ISession session)
{
changeIdentity.Invoke(newIdentity);
session.Flush();
session.Clear();
}
}
您应该根据不可变数据实现Equals
和GetHashCode
。无法以合理的方式更改哈希值。
答案 2 :(得分:0)
我花了很长时间才得到它,但我认为我的问题的答案实际上看似简单。 Hibernate团队长期以来一直倡导的最佳方法就是不要覆盖equals和gethashcode。我没有得到的是,当我在一组业务对象上调用Contains时,显然我想知道该集合是否包含具有特定业务价值的对象。但这是我从Nhibernate持久性集中得不到的东西。但Stefan Steinegger在关于这个问题的另一个问题的评论中说得对,我问:“持久性集合不是商业集合”!我第一次完全没理解他的评论。
关键问题是我不应该尝试将持久性设置为业务集合。相反,我应该使用包含在业务集合中的持久性集。事情变得容易多了。所以,在我的代码中我创建了一个包装器:
internal abstract class EntityCollection<TEnt, TParent> : IEnumerable<TEnt>
{
private readonly Iesi.Collections.Generic.ISet<TEnt> _set;
private readonly TParent _parent;
private readonly IEqualityComparer<TEnt> _comparer;
protected EntityCollection(Iesi.Collections.Generic.ISet<TEnt> set, TParent parent, IEqualityComparer<TEnt> comparer)
{
_set = set;
_parent = parent;
_comparer = comparer;
}
public IEnumerator<TEnt> GetEnumerator()
{
return _set.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public bool Contains(TEnt entity)
{
return _set.Any(x => _comparer.Equals(x, entity));
}
internal Iesi.Collections.Generic.ISet<TEnt> GetEntitySet()
{
return _set;
}
internal protected virtual void Add(TEnt entity, Action<TParent> addParent)
{
if (_set.Contains(entity)) return;
if (Contains(entity)) throw new CannotAddItemException<TEnt>(entity);
_set.Add(entity);
addParent.Invoke(_parent);
}
internal protected virtual void Remove(TEnt entity, Action<TParent> removeParent)
{
if (_set.Contains(entity)) return;
_set.Remove(entity);
removeParent.Invoke(_parent);
}
}
这是一个实现集合的业务含义的通用包装器。它通过IEqualityComparer知道两个业务对象何时通过值相等,它将自身呈现为真实的业务集合,将实体暴露为可枚举的实体接口(比暴露持久性集更清晰),它甚至知道如何处理双向关系父母。
拥有此业务集合的父实体具有以下代码:
public virtual IEnumerable<IProduct> Products
{
get { return _products; }
}
public virtual Iesi.Collections.Generic.ISet<Product> ProductSet
{
get { return _products.GetEntitySet(); }
protected set { _products = new ProductCollection<Brand>(value, this); }
}
public virtual void AddProduct(IProduct product)
{
_products.Add((Product)product, ((Product)product).SetBrand);
}
public virtual void RemoveProduct(IProduct product)
{
_products.Remove((Product)product, ((Product)product).RemoveFromBrand);
}
因此,实体实际上有两个接口,一个公开业务集合的业务接口和一个暴露给Nhibernate以处理集合持久性的实体接口。请注意,使用ProductSet属性传递的Nhibernate返回相同的持久性集。
它基本上归结为分离关注点:
只有当我想在会话之间混合实体时,我将不得不求助于上面提到的其他解决方案。但我认为如果你能避免这种情况,你应该这样做。