NHibernate map by code with OneToMany association: Select is inefficient and Inserts fail (NHibernate.StaleStateException) - nhibernate

I have a parent class and a child class. One Child is always related to just one parent, but a parent can have multiple children:
public class Parent
{
public virtual string Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Child> Children { get; set; } = new List<Child>();
}
public class Child
{
public virtual int Id { get; set; }
public virtual string ParentId { get; set; }
public virtual string Name { get; set; }
}
I'm using the latest version of NHibernate 5.1.3 and mapping by code:
internal class ParentMapping : ClassMapping<Parent>
{
public ParentMapping()
{
Table("Parent");
Id(x => x.Id);
Property(x => x.Name);
Bag(
x => x.Children,
map =>
{
map.Key(km => km.Column("ParentId"));
map.Lazy(CollectionLazy.NoLazy);
map.Cascade(Cascade.Persist);
map.Inverse(true);
},
x => x.OneToMany());
}
}
internal class ChildMapping : ClassMapping<Child>
{
public ChildMapping()
{
Table("Child");
Id(x => x.Id, x => x.Generator(Generators.Identity));
Property(x => x.ParentId);
Property(x => x.Name);
}
}
Queries work but are quite inefficient. Instead of creating a single JOIN statement to query the children together with their parent, an explicit SELECT is made to retrieve the Child objects.
Even worse, inserts result in the following error:
NHibernate.StaleStateException: 'Batch update returned unexpected row count from update; actual row count: 0; expected: 3'
Here's a sample of a query:
using (var session = _sessionProvider.GetSession())
return session.Query<T>().ToList();
And that's the code to save a new item:
using (var session = _sessionProvider.GetSession())
{
session.Transaction.Begin();
session.Save(newEntity);
session.Transaction.Commit();
}
So everything's pretty easy.
I assume, the Bag() configuration in ParentMapping needs to be fixed. What am I doing wrong?

Change the fetch strategy of the bag mapping to join will generate a join query, like this:
Bag(
e => e.Children,
map => {
map.Key(km => km.Column("ParentId"));
// map.Lazy(CollectionLazy.NoLazy);
// change fetch str
map.Fetch(CollectionFetchMode.Join);
map.Cascade(Cascade.Persist);
map.Inverse(true);
},
x => x.OneToMany()
);
And you have the collection persist as inverse (map.Inverse(true);), you should have a many to one mapping for Parent in your Child class like this:
public class Child {
public virtual int Id { get; set; }
// public virtual string ParentId { get; set; }
public virtual Parent Parent { get; set; }
public virtual string Name { get; set; }
}
Then map the Parent property as ManyToOne like this:
public ChildMapping() {
// other mapping goes here
ManyToOne(
x => x.Parent,
map => {
map.Column("ParentId");
map.Class(typeof(Parent));
map.Fetch(FetchKind.Join);
}
);
}
But nhibernate do not include children of parent by default (maybe it is too heavy), and if you want to query children with parent instance, you can query like this:
using (var session = OpenSession()) {
var query = session.Query<Parent>().Select(p => new {
Parent = p, Children = p.Children
});
var data = query.ToList();
}
For saving entities to database, should do like this:
try {
// save parent first
var parent = new Parent();
parent.Name = "Parent object";
session.Save(parent);
// then save child
var child = new Child();
child.Name = "Child object";
child.Parent = parent;
session.Save(child);
session.Flush();
tx.Commit();
}
catch (Exception) {
tx.Rollback();
throw;
}

Related

NHibernate mapping - self referencing: parent and children

I am trying to have this kind of Model:
public class Activity
{
public virtual int ID { get; set; }
public virtual int? ParentID { get; set; }
public virtual int? RootID { get; set; }
public virtual Activity Parent { get; set; }
public virtual Activity Root { get; set; }
public virtual IList<Activity> Children { get; set; }
}
If you are looking at it from a structure point of view, it is a tree.
The root element does not have a parent or a root, but may have children.
Any of its children must have a parent and a root (for the first level children root = parent)
The mapper is like this:
public class ActivityMap : ClassMapping<Activity>
{
public ActivityMap()
{
Table("activity");
Lazy(true);
Id(x => x.ID, map => map.Generator(Generators.Identity));
ManyToOne(x => x.Root, map => { map.Column("RootID"); map.Cascade(Cascade.All); });
Bag(x => x.Children,
mapping =>
{
mapping.Inverse(false);
mapping.Lazy(CollectionLazy.Lazy);
mapping.Key(k => k.Column("ParentID"));
mapping.Cascade(Cascade.All);
},
mapping => mapping.ManyToMany(map=>map.Class(typeof(Activity)))
);
}
}
The problem is when I try to fetch the children, the sql statement looks like:
SELECT children0_.ParentID as ParentID1_,
children0_.elt as elt1_,
activity1_.ID as ID55_0_,
activity1_.TaskID as TaskID55_0_,
activity1_.ActivityTypeID as Activity3_55_0_,
activity1_.StateID as StateID55_0_,
activity1_.Continueforward as Continue5_55_0_,
activity1_.Ordernumber as Ordernum6_55_0_,
activity1_.IsDeleted as IsDeleted55_0_,
activity1_.Created as Created55_0_,
activity1_.Modified as Modified55_0_,
activity1_.StartTime as StartTime55_0_,
activity1_.EndTime as EndTime55_0_,
activity1_.Progress as Progress55_0_,
activity1_.RootID as RootID55_0_
FROM Children children0_ left outer join activity activity1_ on children0_.elt=activity1_.ID WHERE children0_.ParentID=?
First of all it seems that it is looking for the Children table which does not exist. Should be Activity table
Second: I am not sure what is with that "elt" column... it does not exist anywhere
Anyone has an idea how to make this mapping?
Later Edit:
found in answer to the second question:
NHibernate elt field
In my later edit I have the answer to my second question.
And for the second question, the solution that I found is to give up to the relation with the Root entity
ManyToOne(x => x.Root, map => { map.Column("RootID"); map.Cascade(Cascade.All); });
and replace it with
Property(x => x.RootID);
because I have no need to have the entire entity for the Root.
Also I have changed the bag for Children like this:
Bag(x => x.Children,
mapping =>
{
mapping.Inverse(false);
mapping.Lazy(CollectionLazy.Lazy);
mapping.Key(k => k.Column("ParentID"));
mapping.Cascade(Cascade.All);
},
mapping => mapping.OneToMany()
);

NHibernate -- duplicate results when using collection cache

I am getting very strange behavior from NHibernate when using the second level cache with multiple layers of bi-directonal parent-child(-grandchild) one-to-many relationships:
int id;
using(var session = sessionFactory.OpenSession())
{
var parent = new Parent();
var child1 = parent.AddChild();
child1.AddGrandChild();
var child2 = parent.AddChild();
child2.AddGrandChild();
session.Save(parent);
session.Flush(); // force id generation
id = parent.Id;
}
using(var session = sessionFactory.OpenSession())
{
var parent = session.Get<Parent>(id);
parent.Children.Should().HaveCount(2); // but is actually 3; second child duplicated
}
The second child, which has two grandchildren, gets duplicated in the collection, but with the same object id (ReferenceEquals() is true). When I disable caching for the Parent.Children collection, or set the fetching strategy to Join, the problem goes away. Any ideas how to find out what is happening here?
Entities:
class Parent
{
public virtual int Id { get; protected set; }
public virtual List<Child> children = new List<Child>();
public virtual IEnumerable<Child> Children
{
get { return children.ToList(); }
}
public virtual Child AddChild()
{
var child = new Child(this);
this.children.Add(child);
return child;
}
}
class Child
{
public Child(Parent parent)
{
this.Parent = parent;
}
public virtual int Id { get; protected set; }
public virtual List<GrandChild> grandChildren = new List<GrandChild>();
public virtual IEnumerable<GrandChild> GrandChildren
{
get { return grandChildren.ToList(); }
}
public virtual Parent Parent { get; protected set; }
public virtual GrandChild AddGrandChild()
{
var grandChild = new GrandChild(this);
this.grandChildren.Add(grandChild);
return grandChild;
}
}
class GrandChild
{
public virtual int Id { get; protected set; }
public virtual Child Parent { get; protected set; }
public GrandChild(Child parent)
{
this.Parent = parent;
}
}
Mappings:
public class ParentMap : ClassMap<Parent>
{
public ParentMap()
{
Id(p => p.Id).GeneratedBy.Identity();
HasMany(p => p.Children)
.Access.CamelCaseField()
.Inverse()
.Cascade.AllDeleteOrphan()
.Cache.ReadWrite();
Cache.ReadWrite();
}
}
public class ChildMap : ClassMap<Child>
{
public ChildMap()
{
Id(c => c.Id).GeneratedBy.Identity();
HasMany(c => c.GrandChildren)
.Access.CamelCaseField()
.Inverse()
.Cascade.AllDeleteOrphan()
.Cache.ReadWrite();
References(c => c.Parent);
Cache.ReadWrite();
}
}
public class GrandChildMap : ClassMap<GrandChild>
{
public GrandChildMap()
{
Id(c => c.Id).GeneratedBy.Identity();
References(c => c.Parent);
Cache.ReadWrite();
}
}
[EDIT]
I worked around this issue by not enabling the cache for the child collections. Since I did it in the first place in order to avoid N+1 select issues with fetch join queries on children of cached entities, I did the following instead:
public ChildMap()
{
// ...
HasMany(c => c.GrandChildren) // removed Cache.ReadWrite() here
/* ... */;
// added IncludeAll() to make sure lazily fetched collections get cached
Cache.ReadWrite().IncludeAll();
}
I still don't understand what's going on here though, so any insight into this matter would still be welcome :-).
Have you tried using transactions instead of seassion flushing? Like
using(var session = sessionFactory.OpenSession())
using(var transaction = session.BeginTransaction())
{
var parent = new Parent();
[...]
parent.Save();
transaction.Commit();
id = parent.Id;
}
Maybe that helps. I once had a similar problem that I could solve with using transactions.

NHibernate.Mapping.ByCode Many-to-Many relations

I've created 2 objects:
public class Set
{
public Set()
{
_sorts = new List<Sort>();
}
public virtual int Id { get; set; }
public virtual string Code { get; set; }
private ICollection<Sort> _sorts;
public virtual ICollection<Sort> Sorts
{
get { return _sorts; }
set { _sorts = value; }
}
}
public class Sort
{
public Sort()
{
_sets = new List<Set>();
}
public virtual int Id { get; set; }
public virtual string Name { get; set; }
private ICollection<Set> _sets;
public virtual ICollection<Set> Sets
{
get { return _sets; }
set { _sets = value; }
}
}
And 2 mappings:
public class SetMapping: ClassMapping<Set>
{
public SetMapping()
{
Table("Sets");
Id(x => x.Id, map => map.Generator(IdGeneratorSelector.CreateGenerator()));
Property(x => x.Code, map =>
{
map.Length(50);
map.NotNullable(false);
});
Bag(x => x.Sorts, map =>
{
map.Key(k =>
{
k.Column("SetId");
k.NotNullable(true);
});
map.Cascade(Cascade.All);
map.Table("SetsToSorts");
map.Inverse(true);
}, r => r.ManyToMany(m => m.Column("SortId")));
}
}
public class SortMapping: ClassMapping<Sort>
{
public SortMapping()
{
Table("Sorts");
Id(x => x.Id, map => map.Generator(IdGeneratorSelector.CreateGenerator()));
Property(x => x.Name, map =>
{
map.Length(50);
map.NotNullable(false);
});
}
}
usage:
Set can have many sorts
Sort can belong to many sets.
And I would like to use this as:
var set = new Set() {Code = "001"};
var sort = new Sort {Name = "My name"};
set.Sorts.Add(sort);
sort.Sets.Add(set);
Somehow the relations are not working yet because when I try to use the above code to add sorts to set for example and commit then I don't see any records saved to the SetsToSorts linked table.
Does anyone have a clue what I'm missing in my mapping? Or otherwise doing wrong?
Thank you,
Joost
Your mapping says that Set's Sort collection is inverse (map.Inverse(true)). That means the other side of the bidirectional association is responsible for persisting changes.
But your Sort class mapping doesn't have any collection mapping. Remove map.Inverse(true) on SetMapping or add noninverse collection mapping to SortMapping.

Fluent NHibernate compositeid to mapped class

I'm trying to figure out how to use CompositeId to map another class. Here's a test case:
The tables:
TestParent:
TestParentId (PK)
FavoriteColor
TestChild:
TestParentId (PK)
ChildName (PK)
Age
The classes in C#:
public class TestParent
{
public TestParent()
{
TestChildList = new List<TestChild>();
}
public virtual int TestParentId { get; set; }
public virtual string FavoriteColor { get; set; }
public virtual IList<TestChild> TestChildList { get; set; }
}
public class TestChild
{
public virtual TestParent Parent { get; set; }
public virtual string ChildName { get; set; }
public virtual int Age { get; set; }
public override int GetHashCode()
{
return Parent.GetHashCode() ^ ChildName.GetHashCode();
}
public override bool Equals(object obj)
{
if (obj is TestChild)
{
var toCompare = obj as TestChild;
return this.GetHashCode() != toCompare.GetHashCode();
}
return false;
}
}
The Fluent NHibernate maps:
public class TestParentMap : ClassMap<TestParent>
{
public TestParentMap()
{
Table("TestParent");
Id(x => x.TestParentId).Column("TestParentId").GeneratedBy.Native();
Map(x => x.FavoriteColor);
HasMany(x => x.TestChildList).KeyColumn("TestParentId").Inverse().Cascade.None();
}
}
public class TestChildMap : ClassMap<TestChild>
{
public TestChildMap()
{
Table("TestChild");
CompositeId()
.KeyProperty(x => x.ChildName, "ChildName")
.KeyReference(x => x.Parent, "TestParentId");
Map(x => x.Age);
References(x => x.Parent, "TestParentId"); /** breaks insert **/
}
}
When I try to add a new record, I get this error:
System.ArgumentOutOfRangeException :
Index was out of range. Must be
non-negative and less than the size of
the collection. Parameter name: index
I know this error is due to the TestParentId column being mapped in the CompositeId and References calls. However, removing the References call causes another error when querying TestChild based on the TestParentId.
Here's the code that does the queries:
var session = _sessionBuilder.GetSession();
using (var tx = session.BeginTransaction())
{
// create parent
var p = new TestParent() { FavoriteColor = "Red" };
session.Save(p);
// creat child
var c = new TestChild()
{
ChildName = "First child",
Parent = p,
Age = 4
};
session.Save(c); // breaks with References call in TestChildMap
tx.Commit();
}
// breaks without the References call in TestChildMap
var children = _sessionBuilder.GetSession().CreateCriteria<TestChild>()
.CreateAlias("Parent", "p")
.Add(Restrictions.Eq("p.TestParentId", 1))
.List<TestChild>();
Any ideas on how to create a composite key for this scenario?
I found a better solution that will allow querying and inserting. The key is updating the map for TestChild to not insert records. The new map is:
public class TestChildMap : ClassMap<TestChild>
{
public TestChildMap()
{
Table("TestChild");
CompositeId()
.KeyProperty(x => x.ChildName, "ChildName")
.KeyReference(x => x.Parent, "TestParentId");
Map(x => x.Age);
References(x => x.Parent, "TestParentId")
.Not.Insert(); // will avoid "Index was out of range" error on insert
}
}
Any reason you can't modify your query to just be
_sessionBuilder.GetSession().CreateCriteria<TestChild>()
.Add(Restrictions.Eq("Parent.TestParentId", 1))
.List<TestChild>()
Then get rid of the reference?

Fluent NHibernate Mapping is set to AllDeleteOrphan but is still trying to null the foreign key in the DB

I'm getting an error with NHibernate when I try to perfrom a ISession.Delete on any table with a One to Many relationship.
NHibernate is trying to set the foreign key to the parent table in the child table to null, rather than just deleting the child table row.
Here is my domain:
public class Parent
{
public Parent()
{
_children = new List<Child>();
}
public int Id { get; set; }
public string SimpleString { get; set; }
public DateTime? SimpleDateTime { get; set; }
private IList<Child> _children;
public IEnumerable<Child> Children
{
get { return _children; }
}
public void AddChild(Child child)
{
child.Parent = this;
_children.Add(child);
}
}
public class Child
{
public int Id { get; set; }
public string SimpleString { get; set; }
public DateTime? SimpleDateTime { get; set; }
[JsonIgnore]
public Parent Parent { get; set; }
}
I have set-up the Fluent NHibernate mappings as follows:
public class ParentMap : ClassMap<Parent>
{
public ParentMap()
{
Not.LazyLoad();
Id(x => x.Id);
Map(x => x.SimpleString);
Map(x => x.SimpleDateTime);
HasMany(x => x.Children)
.Not.LazyLoad()
.KeyColumn("ParentId").Cascade.AllDeleteOrphan()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
}
}
public class ChildMap : ClassMap<Child>
{
public ChildMap()
{
Not.LazyLoad();
Id(x => x.Id);
Map(x => x.SimpleString);
Map(x => x.SimpleDateTime);
References(x => x.Parent).Not.Nullable().Column("ParentId").Cascade.All().Fetch.Join();
}
}
I've told NHibernate to Cascade.AllDeleteOrphan() but it's still trying to set the ParentId foriegn key to null here is the test I setup:
public void Delete_GivenTableWithChildren_WillBeDeletedFromDB()
{
int id;
using (var createSession = MsSqlSessionProvider.SessionFactory.OpenSession())
{
var parent = new Parent();
parent.AddChild(new Child { SimpleString = "new child from UI" });
using (var trx = createSession.BeginTransaction())
{
createSession.Save(parent);
trx.Commit();
id = parent.Id;
}
}
using (var firstGetSession = MsSqlSessionProvider.SessionFactory.OpenSession())
{
var result = firstGetSession.Get<Parent>(id);
Assert.IsNotNull(result);
}
using (var deleteSession = MsSqlSessionProvider.SessionFactory.OpenSession())
{
using (var trx = deleteSession.BeginTransaction())
{
deleteSession.Delete("from " + typeof(Parent).Name + " o where o.Id = :Id", id, NHibernateUtil.Int32);
trx.Commit();
}
}
using (var session = MsSqlSessionProvider.SessionFactory.OpenSession())
{
var result = session.Get<Parent>(id);
Assert.IsNull(result);
}
}
Which is failing on the deleteSession.Delete line after attempting the following SQL:
exec sp_executesql N'UPDATE [Child] SET ParentId = null WHERE ParentId = #p0',N'#p0 int',#p0=5
with:
NHibernate.Exceptions.GenericADOException : could not delete collection: [SaveUpdateOrCopyTesting.Parent.Children#5][SQL: UPDATE [Child] SET ParentId = null WHERE ParentId = #p0]
----> System.Data.SqlClient.SqlException : Cannot insert the value NULL into column 'ParentId', table 'SaveUpdateCopyTestingDB.dbo.Child'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
Does anyone know what I've done wrong in my mappings, or know of a way to stop NHibernate from attempting to null the foreign key id?
Thanks
Dave
Try setting .Inverse() on the ParentMap's HasMany, so it looks like:
HasMany(x => x.Children)
.Not.LazyLoad()
.KeyColumn("ParentId").Cascade.AllDeleteOrphan().Inverse()
.Access.ReadOnlyPropertyThroughCamelCaseField(Prefix.Underscore);
I'm not sure if cascade works if you delete with HQL.
Try this:
var parent = deleteSession.Load<Parent>(id)
deleteSession.Delete(parent);
It's a pity that you don't have lazy loading. Load would not need the entity to be read from the database, it would just create a proxy in memory.