2

私が提案するソリューションにはかなりのコードが含まれますが、SqLiteがインストールされていれば、すべてをコピーしてVSテストソリューションに貼り付けることができ、自分でテストを実行できるはずです。

Nhibernateを使用してオブジェクトIDとオブジェクトの同等性およびデータベースIDの問題に苦労しているので、さまざまな投稿を読みました。ただし、コレクションと組み合わせてオブジェクトIDを適切に設定する方法を明確に把握することはできませんでした。基本的に、私が得た大きな問題は、オブジェクトがコレクションに追加されると、そのID(GetHashCodeによって派生)メソッドが変更できないことです。GetHasHCodeを実装するための推奨される方法は、ビジネスキーを使用することです。しかし、ビジネスキーが適切でない場合はどうなるでしょうか。そのエンティティを新しいビジネスキーで更新してもらいたいのですが。しかし、そのオブジェクトのIDの不変性に違反したため、コレクションが同期しなくなりました。

以下のコードは、この問題を解決するための提案です。ただし、私は確かに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_")));

        }
    }
}

重要な編集!正しいアプローチではないと指摘されているように、私は今、私が提案していたアプローチを検討します。私が直面していたジレンマに対して、私は別の答えを提供しました。

4

3 に答える 3

3

これは興味深いアプローチですが、時間をかけて理解して批評する代わりに、この問題の解決策を提供します。

ジェネリックエンティティの基本クラスのアイデアは好きではないので、私のソリューションはint、Guid、および文字列のIDのみをサポートします。ハッシュコードを取得するためにを使用するなど、以下のコードの一部は、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アイデンティティ:

[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 ID:

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

文字列ID:

[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());
    }
}
于 2011-08-29T13:11:29.200 に答える
1

ここでの基本的な誤解は、ビジネスデータに基づいてEqualsとGetHashCodeを実装することだと思います。なぜあなたがそれを好むのか分かりません、私はそれに利点を見ることができません。Idを持たない値オブジェクトを扱う場合を除いて-もちろん-。

アイデンティティフィールド、平等、ハッシュコードに関する素晴らしい投稿がnhforge.orgにあります

編集:コードのこの部分は問題を引き起こします:

    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. エンティティがセッションで見つからなくなったため、dbクエリが多すぎる可能性があります。
    2. データベースから読み取られたエンティティが別のエンティティにリンクされていて、NHが一時的なものであると文句を言うと、混乱が生じる可能性があります。
    3. たとえば、ループで発生した場合、メモリリークが発生する可能性があります

不変のデータを実装Equalsし、それGetHashCodeに基づいている必要があります。ハッシュを変更することは合理的な方法では不可能です。

于 2011-08-30T08:33:38.590 に答える
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を介して2つのビジネスオブジェクトの値が等しい場合、エンティティをエンティティインターフェイスの列挙可能として公開する真のビジネスコレクションとして表示され(永続性セットを公開するよりもはるかにクリーンです)、親。

このビジネスコレクションを所有する親エンティティには、次のコードがあります。

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

したがって、エンティティには実際には2つのインターフェイスがあります。ビジネスコレクションを公開するビジネスインターフェイスと、コレクションの永続性を処理するためにNhibernateに公開されるエンティティインターフェイスです。ProductSetプロパティを使用して渡されたものと同じ永続セットがNhibernateに返されることに注意してください。

基本的に、すべては関心の分離に要約されます。

  • 永続性セットは私の関心事ではありませんが、nhibernateによって処理されて私のコレクションを永続化します
  • 価値による平等のビジネス上の意味は、平等比較者によって処理されます
  • セットのビジネス上の意味、つまり、セットに同じビジネス価値を持つエンティティがすでに含まれている場合、同じビジネス価値を持つ2番目の異なるオブジェクトを渡すことができないようにする必要があります。これは、ビジネスコレクションオブジェクトによって処理されます。

ただ、セッション間でエンティティを混在させたい場合は、上記のように他のソリューションに頼らなければなりません。しかし、そのような状況を回避できれば、そうすべきだと思います。

于 2011-09-01T06:49:02.337 に答える