Решает ли это проблему идентичности Nhibernate и проблемы GetHashCode?
Предлагаемое мной решение включает в себя довольно много кода, но вы можете просто скопировать его и вставить в тестовое решение 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_")));
}
}
}
Важное редактирование! Сейчас я рассматриваю предложенный мной подход, который был отмечен как неправильный подход. Я дал другой ответ на дилемму, с которой я столкнулся.
3 ответа
Я думаю, что основное неправильное представление заключается в том, что вы реализуете Equals и GetHashCode на основе бизнес-данных. Я не знаю, почему вы предпочитаете это, я не вижу в этом никакого преимущества. За исключением, конечно, при работе с объектом-значением, у которого нет идентификатора.
На nhforge.org есть отличный пост о Identity Field, Equality и 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();
}
}
- Промывка стоит дорого
- При очистке сеанса NH снова загружает те же объекты.
- Это может привести к слишком большому количеству запросов к базе данных, потому что объекты больше не найдены в сеансе.
- Это может привести к путанице, когда объект, который был прочитан из БД, связан с другим, и NH жалуется, что он временный
- Это может привести к утечкам памяти, например, когда это происходит в цикле
Вы должны реализовать Equals
а также GetHashCode
на основе неизменных данных. Изменение хэша не возможно разумным способом.
Это интересный подход, но вместо того, чтобы тратить время на понимание и критику, я просто предложу свое решение этой проблемы.
Мне не нравится идея базового базового класса сущностей, поэтому мое решение поддерживает только 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());
}
}
Мне потребовалось много времени, чтобы получить его, но я думаю, что ответ на мою проблему на самом деле обманчиво прост. Наилучший подход, который давно поддерживается командой Hibernate, это просто не переопределять equals и gethashcode. Что я не получил, так это то, что когда я вызываю Contains для набора бизнес-объектов, очевидно, я хочу знать, содержит ли этот набор объект с определенной бизнес-ценностью. Но это было то, чего я не получил от набора настойчивости Nhibernate. Но Стефан Штайнеггер исправил это в комментарии по другому вопросу на эту тему, который я задавал: "набор постоянства - это не бизнес-коллекция"! Я совершенно не понял его замечания с первого раза.
Ключевой проблемой было то, что я не должен пытаться настроить постоянство на бизнес-коллекцию. Вместо этого я должен использовать набор постоянства, завернутый в бизнес-коллекцию. Тогда все становится намного проще. Итак, в моем коде я создал обертку:
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 для обеспечения устойчивости коллекции. Обратите внимание, что в Nhibernate возвращается тот же набор сохраняемости, что и при использовании свойства ProductSet.
В основном все сводится к разделению интересов:
- набор настойчивости не моя забота, но nhibernate обрабатывает, чтобы сохранить мою коллекцию
- бизнес-смысл равенства по значению обрабатывается средствами сравнения равенства
- бизнес-значение набора, т. е. когда набор уже содержит сущность с одинаковым бизнес-значением, я не должен иметь возможность передавать второй другой объект с таким же бизнес-значением, который обрабатывается объектом бизнес-коллекции.
Только когда я хочу смешивать объекты между сеансами, мне придется прибегать к другим решениям, как упомянуто выше. Но я думаю, что если вы можете избежать этой ситуации, вы должны.