【问题标题】:Does this solve Nhibernate identity problem and GetHashCode issues?这是否解决了 Nhibernate 身份问题和 GetHashCode 问题?
【发布时间】:2011-08-29 12:36:40
【问题描述】:

我提出的解决方案涉及相当多的代码,但假设您安装了 SqlLite,您可以将其全部复制并粘贴到 VS 测试解决方案中,并且您应该能够自己运行测试。

由于我一直在努力解决使用 Nhibernate 的对象身份与对象相等和数据库身份问题,因此我阅读了各种帖子。但是,我无法清楚地了解如何结合集合正确设置对象标识。基本上,我得到的最大问题是,一旦将对象添加到集合中,它的标识(由 GetHashCode 派生)方法就不能改变。实现 GetHasHCode 的首选方法是使用业务密钥。但是,如果业务密钥不正确怎么办?我想用它的新业务密钥更新该实体。但是后来我的集合不同步,因为我违反了该对象身份的不变性。

以下代码是解决此问题的建议。但是,由于我当然不是 NHibernate 专家,也不是非常有经验的开发人员,因此我很乐意从更高级的开发人员那里获得 cmets,无论这是否可行。

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_")));

        }
    }
}

重要的编辑!我现在考虑我建议的方法,正如已经指出的那样不是正确的方法。对于我面临的困境,我提供了不同的答案。

【问题讨论】:

  • 您是否在多个 NH 会话中使用相同的实体实例?
  • 不,我没有,但我认为它应该适用于我建议的方法。
  • 我还发现了为什么在 Assert.IsTrue 上测试 ThatBrandCannotBeFoundIfIdentityChanges 失败。在 GetHashCode 之后调用 Equals。 GetHash 使用旧名称检索对象,然后在 Equals 方法中使用新名称。这强化了您无法更改身份的概念,除非您刷新并清除对该对象的所有引用。这是在小的 ChangeIdentifier 实用程序类中实现的。

标签: nhibernate gethashcode


【解决方案1】:

这是一种有趣的方法,但我不会花时间去理解和批评,我只会提供我对这个问题的解决方案。

我不喜欢通用实体基类的想法,所以我的解决方案只支持 int、Guid 和字符串标识。下面的一些代码,例如使用Func&lt;int&gt; 来获取哈希码,仅用于支持不区分大小写的字符串比较。如果我忽略字符串标识符(我希望我可以),代码会更紧凑。

这段代码通过了我为它进行的单元测试,并没有让我在我们的应用程序中失望,但我确信存在边缘情况。我唯一想到的是:如果我新建并保存一个实体,它将保留其原始哈希码,但如果在保存后我在另一个会话中从数据库中检索同一实体的实例,它将具有不同的哈希代码。

欢迎反馈。

基类:

[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 身份:

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

【讨论】:

  • 事实上我尝试了你的解决方案,但在我让 NH 生成身份并向集合添加新实体的场景中遇到了问题。我必须检查每个子实体是否已包含在存储库中(即已被持久化)。但是,一旦实体被持久化(添加到存储库),哈希码就会在您的场景中更改。因为您从数据库 id 派生身份。我认为这篇文章解决了我在您的解决方案中遇到的问题:stackoverflow.com/questions/2100226/…
  • 我的方案是保证哈希码在持久化后不会改变,这就是缓存它的目的。
  • 知道了,你说得对。只是,我还希望看到我的集合上的 contains 方法真正回答了这个问题,是否包含此业务标识?因此,据我所知,您需要基于具有某种语义意义的合理业务密钥来实现哈希码。谢谢——卡斯帕
  • +1,它应该可以工作。 IMO,所有这些基类都有点复杂。它对应于 nhforge 上的这篇博文:nhforge.org/blogs/nhibernate/archive/2008/09/06/…
【解决方案2】:

我认为这里的基本误解是您基于业务数据实现 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();
        }
    }
  1. 冲洗很昂贵
  2. 清除会话会使 NH 再次加载相同的实体。
    1. 它可能会产生太多的数据库查询,因为在会话中找不到实体了。
    2. 当从 db 读取的实体链接到另一个实体并且 NH 抱怨它是暂时的时,可能会产生混淆
    3. 它可能会产生内存泄漏,例如当它发生在循环中时

您应该基于不可变数据实现EqualsGetHashCode。无法以合理的方式更改哈希。

【讨论】:

  • 好的,我引用了 -- NHibernate in Action -- on using database identity for equals and gethashcode:“不幸的是,这个解决方案有一个大问题:NHibernate 不会分配标识符值,直到实体已保存......我们强烈反对这种解决方案(数据库标识符相等)。”我认为,他们推荐我建议的方法:“要获得我们推荐的解决方案,您需要了解业务密钥的概念”。没有直接给出明确解决方案的大问题是如何实施身份变更。但是,我想我已经知道了。
  • 我认为类似的讨论也很好地总结了各种方法的优缺点。我想在这个线程的答案中探索建议的方法 3:stackoverflow.com/questions/2100226/…
  • 即使您采用业务密钥方法:密钥也不应更改。一个类也不应该改变它的 HashCode。我不知道 Set,但字典缓存了 HashCodes,如果它发生变化,它会全部中断。
  • Set 的工作原理是一样的,一旦一个对象在一个 set 中,不要更改 hashcode,但如果 hash code 被更改,则丢弃该 set 并重新加载该 set。基本上这就是我所做的。这是使用您必须处理的业务密钥的缺点。
  • 我会说:“扔掉套装”是不行的。刷新和清除会话不仅会影响性能,还会产生副作用,因为它会再次实例化相同的实体。
【解决方案3】:

我花了很长时间才得到它,但我认为我的问题的答案实际上很简单。正如 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 相同。

基本上都归结为关注点分离:

  • 持久性集不是我关心的问题,而是由 nhibernate 处理以持久化我的集合
  • 值相等的业务含义由相等比较器处理
  • 集合的业务含义,即当集合已经包含具有相同业务价值的实体时,我应该不能传入具有相同业务价值的第二个不同对象,由业务集合对象处理。

只有当我想在会话之间混合实体时,我才不得不求助于上面提到的其他解决方案。但我认为,如果你能避免这种情况,你应该这样做。

【讨论】:

    猜你喜欢
    • 2022-10-17
    • 1970-01-01
    • 1970-01-01
    • 2017-02-05
    • 2022-08-04
    • 1970-01-01
    • 2022-01-19
    相关资源
    最近更新 更多