problem with NHibernate one-to-many relationship Collection with cascade
I have AssetGroup entity with has a one-to-many relation with Asset entity. There is a Entity base class which overrides Equals and GetHashCode . I am following the example of ch 20 parent child
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="TestNHibernate"
namespace="TestNHibernate.Models" auto-import="true">
<class name="AssetGroup">
<id name="Id" column="Id" type="guid">
<generator class="guid"></generator>
</id>
<property name="Name" type="string" not-null="true"/>
<set name="Assets" cascade="all" inverse="true" access="field.camelcase-underscore" lazy="true">
<key column="AssetGroupID"/>
<one-to-many class="Asset"/>
</set>
</class>
</hibernate-mapping>
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernat开发者_JAVA技巧e-mapping-2.2"
assembly="TestNHibernate"
namespace="TestNHibernate.Models" auto-import="true">
<class name="Asset">
<id name="Id" column="Id" type="guid">
<generator class="guid"></generator>
</id>
<property name="Name" type="string" not-null="true"/>
<many-to-one name="AssetGroup" column="AssetGroupID" cascade="all" lazy="false"/>
</class>
</hibernate-mapping>
the code as folloow:
public class AssetGroup : Entity<Guid>
{
public AssetGroup()
{
this._assets = new HashedSet<Asset>();
}
virtual public string Name { get; set; }
private ISet<Asset> _assets;
virtual public ISet<Asset> Assets
{
get { return _assets; }
protected set { _assets = value; }
}
virtual public bool AddAsset(Asset asset)
{
if (asset != null && _assets.Add(asset))
{
asset.SetAssetGroup(this);
return true;
}
return false;
}
virtual public bool RemoveAsset(Asset asset)
{
if (asset != null && _assets.Remove(asset))
{
asset.SetAssetGroup(null);
return true;
}
return false;
}
}
public class AssetGroup : Entity<Guid>
{
public AssetGroup()
{
this._assets = new HashedSet<Asset>();
}
virtual public string Name { get; set; }
private ISet<Asset> _assets;
virtual public ISet<Asset> Assets
{
get { return _assets; }
protected set { _assets = value; }
}
virtual public bool AddAsset(Asset asset)
{
if (asset != null && _assets.Add(asset))
{
asset.SetAssetGroup(this);
return true;
}
return false;
}
virtual public bool RemoveAsset(Asset asset)
{
if (asset != null && _assets.Remove(asset))
{
asset.SetAssetGroup(null);
return true;
}
return false;
}
}
My TestCode is as follow :
[TestMethod]
public void Can_Use_ISession()
{
ISession session = TestConfig.SessionFactory.GetCurrentSession();
var ag = new AssetGroup { Name = "NHSession" };
session.Save(ag);
var a1 = new Asset { Name = "s1" };
var a2 = new Asset { Name = "s2" };
a1.SetAssetGroup(ag);
a2.SetAssetGroup(ag);
session.Flush();
Assert.IsTrue(a1.Id != default(Guid)); // ok
Assert.IsTrue(a2.Id != default(Guid)); // ok
var enumerator = ag.Assets.GetEnumerator();
enumerator.MoveNext();
Assert.IsTrue(ag.Assets.Contains(enumerator.Current)); // failed
Assert.IsTrue(ag.Assets.Contains(a1)); // failed
Assert.IsTrue(ag.Assets.Contains(a2)); // failed
var agRepo2 = new NHibernateRepository<AssetGroup>(TestConfig.SessionFactory, new QueryFactory(TestConfig.Locator));
Assert.IsTrue(agRepo2.Contains(ag)); // ok
var ag2 = agRepo2.FirstOrDefault(x => x.Id == ag.Id);
Assert.IsTrue(ag2.Assets.FirstOrDefault(x => x.Id == a1.Id) != null); // ok
Assert.IsTrue(ag2.Assets.FirstOrDefault(x => x.Id == a2.Id) != null); // ok
var aa1 = session.Get<Asset>(a1.Id);
var aa2 = session.Get<Asset>(a2.Id);
Assert.IsTrue(ag2.Assets.Contains(aa1)); // failed
Assert.IsTrue(ag2.Assets.Contains(aa2)); // failed
}
My Entity base class is here:
public abstract class Entity<Tid> : IEquatable<Entity<Tid>>
{
[HiddenInput(DisplayValue = false)]
public virtual Tid Id { get; protected set; }
public override bool Equals(object obj)
{
if (obj == null)
return base.Equals(obj);
return Equals(obj as Entity<Tid>);
}
public static bool IsTransient(Entity<Tid> obj)
{
return obj != null && Equals(obj.Id, default(Tid));
}
private Type GetUnproxiedType()
{
return GetType();
}
public virtual bool Equals(Entity<Tid> other)
{
if (ReferenceEquals(this, other))
return true;
if (!IsTransient(this) && !IsTransient(other) && Equals(Id, other.Id))
{
var otherType = other.GetUnproxiedType();
var thisType = GetUnproxiedType();
return thisType.IsAssignableFrom(otherType) || otherType.IsAssignableFrom(thisType);
}
return false;
}
public override int GetHashCode()
{
if (Equals(Id, default(Tid)))
{
return base.GetHashCode();
}
else
{
return Id.GetHashCode();
}
}
}
I have commented which parts have failed in the code . Please help. It seems entities which are saved by cascading is not compatible with ICollection Contains/Remove . Asset a1 a2 are saved and they are inside the parent's Collection . For example I can find them by Linq FirstOrDefault . But the Collection's Contains and Remove will fail to find them . I notice the Collection use GetHashCode while Contains() or Remove() are called.
Assert.IsTrue(ag.Assets.Contains(a1)); // failed
This could would indeed fail. You have to manage the bi-directional relationship. By this I mean,
In AssetGroup:
virtual public bool AddAsset(Asset asset)
{
if (asset != null && _assets.Add(asset))
{
asset.AssetGroup = this;
return true;
}
return false;
}
... and a corresponding remove
In Asset:
virtual public bool SetAssetGroup(AssetGroup group)
{
this.AssetGroup = group;
group.Assets.Add(this);
}
Note the difference between your code and the one above. This is not the only way of doing it, but it's the most mapping-agnostic, safe way of doing it... so whether you set inverse=true on your mapping or not, it'll work. I do it by default without even thinking about it too much.
When working with the model from the outside, you use AddXXX, RemoveXXX, SetXXX. When working with the model from the inside you reference the properties and collections directly. Adopt this as a convention, and you'll be ok for most of the common bi-directional mapping scenarios.
Having said this, I'm not sure why this code fails:
Assert.IsTrue(ag2.Assets.Contains(aa1));
ag2 is from a new query, so that should be ok ... unless the session cached the object, which I don't think it does... but I'm not sure.
I added session.Refresh(ag) and then it works . Is session Refresh an expensive operation ? Is there any alternative
[TestMethod]
public void Can_Use_ISession()
{
ISession session = TestConfig.SessionFactory.GetCurrentSession();
var ag = new AssetGroup { Name = "NHSession" };
session.Save(ag);
var a1 = new Asset { Name = "s1" };
var a2 = new Asset { Name = "s2" };
a1.SetAssetGroup(ag);
a2.SetAssetGroup(ag);
session.Flush();
session.Refresh(ag);
Assert.IsTrue(a1.Id != default(Guid)); // ok
Assert.IsTrue(a2.Id != default(Guid)); // ok
var enumerator = ag.Assets.GetEnumerator();
enumerator.MoveNext();
Assert.IsTrue(ag.Assets.Contains(enumerator.Current)); // failed
Assert.IsTrue(ag.Assets.Contains(a1)); // failed
Assert.IsTrue(ag.Assets.Contains(a2)); // failed
var agRepo2 = new NHibernateRepository<AssetGroup>(TestConfig.SessionFactory, new QueryFactory(TestConfig.Locator));
Assert.IsTrue(agRepo2.Contains(ag)); // ok
var ag2 = agRepo2.FirstOrDefault(x => x.Id == ag.Id);
Assert.IsTrue(ag2.Assets.FirstOrDefault(x => x.Id == a1.Id) != null); // ok
Assert.IsTrue(ag2.Assets.FirstOrDefault(x => x.Id == a2.Id) != null); // ok
var aa1 = session.Get<Asset>(a1.Id);
var aa2 = session.Get<Asset>(a2.Id);
Assert.IsTrue(ag2.Assets.Contains(aa1)); // failed
Assert.IsTrue(ag2.Assets.Contains(aa2)); // failed
}
精彩评论