Fluent NHibernate cascaded save issue with non-unique objects in unique collections - fluent-nhibernate

So my data model looks like this:
HEADER -- (has many) --> TASK -- (has zero or one) --> COMPONENT
However even though every TASK may only have one COMPONENT, a COMPONENT may be associated with various TASKs... and when I perform a SaveOrUpdate() I get the following error:
"a different object with the same identifier value was already associated with the session"
which I validated as true, because more than one TASK does has the same COMPONENT (different object instances, but internally identical). This is initially being read out of the database, which is where my mappings stem from, but apparently I'm missing something in order to save back to the database.
Hopefully someone can point me in the right direction, as the only way I can think to do this is to save the COMPONENTs separately flushing the session in between each TASK.
Here are my mappings:
HEADER
public class Header_ORM: ClassMap<Header> {
public Header_ORM() {
Table("HEADER");
Id(x => x.HeaderID).Length(8);
.
. More Mappings
.
HasMany<Task>(x => x.Tasks)
.Cascade.SaveUpdate()
.KeyColumns.Add("HeaderID")
.LazyLoad();
}
TASK
public class Task_ORM : ClassMap<Task> {
public Task_ORM() {
Table("TASK");
CompositeId(x => x.ID)
.KeyProperty(x => x.HeaderID, x => x.Length(8))
.KeyProperty(x => x.TaskID, x => x.Length(2));
Map(x => x.HeaderID).Length(8).ReadOnly();
Map(x => x.TaskID).Length(2).ReadOnly();
.
. More Mappings
.
References(x => x.EquipmentComponent)
.Cascade.SaveUpdate()
.NotFound.Ignore()
.LazyLoad()
.Columns(
"A",
"B",
"C",
.
. More Columns
.
);
}
}
COMPONENT
public class Component_ORM : ClassMap<Component> {
public Component_ORM() {
Table("COMPONENT");
CompositeId(x => x.ID)
.KeyProperty(x => x.A, x => x.Length(3))
.KeyProperty(x => x.B, x => x.Length(6))
.KeyProperty(x => x.C, x => x.Length(6))
.
. More Composite Key Columns
.
Map(x => x.A).Length(3).ReadOnly();
Map(x => x.B).Length(6).ReadOnly();
Map(x => x.C).Length(6).ReadOnly();
.
. More Mappings
.
}
}

Okay, so I did basically what I said I thought would work (and it did):
using (ITransaction transaction = session.BeginTransaction()) {
foreach (WorkOrderTask t in WorkOrder.Tasks) {
if (t.EquipmentComponent != null) {
session.SaveOrUpdate(t.EquipmentComponent);
session.Flush();
session.Clear();
}
}
session.SaveOrUpdate(WorkOrder);
transaction.Commit();
}
saving each component first, flushing and clearing the session between saves. This was required due to the fact that I am being given the session, which is stateful. It appears a more proper solution is to use a stateless session. I haven't investigated that method further, as I wouldn't be able to use it anyways, but it is worth noting.

Related

Parent-Child mapping with Fluent NHibernate does not insert children

I'm trying to map a very basic parent-child relation with Fluent NHibernate.
However, when analyzing the SQL, only the parent-INSERT statement is created.
The situation is a simple class with a list of other classes. No relation back to the parent is needed. The children needs to be inserted/updated when the parent is inserted/updated.
var room = new Room();
room.Name = "Room1";
room.Courses.Add(new Course(){ Name = "Course1"});
room.Courses.Add(new Course(){ Name = "Course2"});
using (var session = sessionFactory.OpenStatelessSession())
{
using (var transaction = session.BeginTransaction())
{
session.Insert(room);
transaction.Commit();
}
}
The mapping looks like this.
public class RoomMapping : ClassMap<Room>
{
public RoomMapping()
{
Table("Rooms");
Id(x => x.Id)
.GeneratedBy.SeqHiLo("seq_rooms", "1000");
Map(x => x.Name);
HasMany(x => x.Courses)
.Cascade.All();
}
}
public class CourseMap : ClassMap<Course>
{
public CourseMap()
{
Table("Courses");
Id(x => x.Id)
.GeneratedBy.SeqHiLo("seq_courses", "1000");
Map(x => x.Name);
}
}
I already played with multiple options of the HasMany, however non with any success.
Sorry people. I just found it out.
I'm working in a Stateless session. So no relationships are managed ;)

Unable to resolve property: Id

I'm getting the following error message:
NHibernate.HibernateException: NHibernate.HibernateException: Unable to resolve property: Id.
This error is thrown from the following line of code:
User userFound = session.QueryOver<User>()
.Where(x => x.Id == testObjects.TestUser.Id)
.SingleOrDefault();
My abbreviated mappings are as follows:
public class UserMap : ClassMap<User>
{
public UserMap()
{
Table("USER_HEADER");
Id(x => x.Id, "USER_ID")
.GeneratedBy.Foreign("UserLocation");
HasOne(x => x.UserLocation)
.PropertyRef(x => x.Id)
.Cascade.All();
}
}
public class LocationMap : ClassMap<Location>
{
public LocationMap()
{
Table("LOC_HEADER");
Id(x => x.Id, "LOC_ID");
HasOne(x => x.User)
.PropertyRef(x => x.Id);
}
}
I was able to query a User object before I added this relationship to Location so I know it has something to do with it but I'm not sure what exactly. I can successfully create a User object that is tied to a Location but cannot query it. Using ISession.Get produces the same error as the above QueryOver statement.
Below is the overall unit test I am running that is failing:
public void Can_Create_User()
{
using (NHibernate.ISession session = SessionFactory.GetCurrentSession())
{
using (NHibernate.ITransaction tran = session.BeginTransaction())
{
session.Save(testObjects.TestValidationDetail);
session.Save(testObjects.TestUser);
tran.Commit();
}
}
using (NHibernate.ISession session = SessionFactory.GetCurrentSession())
{
User userFound = session.QueryOver<User>().Where(x => x.Id == testObjects.TestUser.Id).SingleOrDefault();
Assert.IsNotNull(userFound);
Assert.AreEqual(userFound.Id, userFound.UserLocation.Id);
}
}
It turns out this was caused by me incorrectly using PropertyRef. In my instance I did not need to use this. The error was being generated because there was no property named Id but there was an ID named Id. I corrected my issues by changing my mappings to:
HasOne(x => x.UserLocation)
.PropertyRef(x => x.Id)
.Cascade.All();
to
HasOne(x => x.UserLocation)
.Cascade.All();
and
HasOne(x => x.User)
.PropertyRef(x => x.Id);
to
HasOne(x => x.User)
PropertyRef maps to property-ref is a legacy feature, it is meant to allow you to create many-to-one associations when the association is not done on the primary key of the association.
I am guessing you want to specify on what property the join is to be made and that is why you used PropertyRef.. if you are using Nhibernates default convention in the mapping for the Id of UserLocation you dont need to explicitly specify the property.. if you are explicitly giving the column name then you need to do the same here, but in that case you need to specify the exact same column name.
Hope that helps..

Only pull Top X using Fluent NHibernate HasMany

I have the following mapping in Fluent-NHibernate Map
public class PostMap : ClassMap<Post>
{
public PostMap()
{
Id(i => i.Id).GeneratedBy.GuidComb();
Map(x => x.SiteId);
Map(x => x.Message);
Map(x => x.DateCreated);
Map(x => x.DateModified);
HasMany(x => x.Comments)
.OrderBy("DateCreated DESC")
.ReadOnly()
.Not.LazyLoad();
}
}
There are a lot of comments on each post, what I would like to do is only bring back the top 5 from the db rather than remove them in code.
You can do it manually. Ignore the property Comments from Post mapping, and load it manually with Take(X):
// query posts
foreach (var post in posts)
{
post.Comments = Session.QueryOver<Comments>()
.Where(x => x.PostId == post.Id)
.Take(X)
.Future();
}
With Future instead of List it will do one DB roundtrip for all comments.
Should be possible with filters. Read here: http://nhibernate.info/doc/nh/en/index.html#filters

NHibernate cascade collection delete when insert new item to not empty collection

I have the following Fluent Mappings:
public ScanDeliverySessionMap()
{
Id(x => x.Id);
...
...
HasManyToMany(x => x.ToScanForms) <--- IList<Form> ToScanForms --->
.Table("ToScanForm")
.ParentKeyColumn("SessionId")
.ChildKeyColumn("FormId").Cascade.SaveUpdate();
}
public FormMap()
{
Id(x => x.Id).Column("FormID").GeneratedBy.Foreign("Log");
....
....
HasManyToMany(x => x.ScanDeliverySessions)
.Table("ToScanForm")
.ParentKeyColumn("FormId")
.ChildKeyColumn("SessionId").Inverse();
}
When I try to insert new Form to the ToScanForms collection
Everything seemingly works properly but watching on NHProf
I see that NH casacde DELETE over all ToScanForms items
and then NH INSERT the ToScanForms items including the new item.
Some screenshots:
That behaviour occurs because nhibernate doesn't know which entities in the collection are new and which are old, so he must delete everything and then re-insert them.
To prevent this is behaviour is quite simple: change your property to an ICollection and map your HasManyToMany as a set. You mapping would be changed to the following:
public ScanDeliverySessionMap()
{
Id(x => x.Id);
...
...
HasManyToMany(x => x.ToScanForms) //<--- ICollection<Form> ToScanForms --->
.AsSet()
.Table("ToScanForm")
.ParentKeyColumn("SessionId")
.ChildKeyColumn("FormId").Cascade.SaveUpdate();
}
public FormMap()
{
Id(x => x.Id).Column("FormID").GeneratedBy.Foreign("Log");
....
....
HasManyToMany(x => x.ScanDeliverySessions)
.AsSet()
.Table("ToScanForm")
.ParentKeyColumn("FormId")
.ChildKeyColumn("SessionId").Inverse();
}
Under the hood nhibernate will use Iesi Collections' HashSet, so now he knows which entities are new and which are old.

Fluent Mapping : using join-table and cascades

having a little trouble with a mapping for the following table setup currently:
Shop
[1] [1]
/ \
[n] [n]
Category-[m]---[n]-Article
The behaviour should be the following :
1 - when deleting a shop, all Articles and Categories Should be deleted
2 - when deleting a Category, related Articles should be unassigned but not deleted
3 - when deleting an Article, related Categories should be unassigned but not deleted
Here's the current mapping:
public class ShopMap: ClassMap<Shop>
{
public ShopMap()
{
this.Table("shop");
Id(x => x.Id).Column("id").GeneratedBy.Native();
Map(x => x.Name).Column("name");
HasMany(x => x.Categories).Cascade.AllDeleteOrphan;
HasMany(x => x.Articles).Cascade.AllDeleteOrphan;
}
}
public class CategoryMap: ClassMap<Category>
{
public CategoryMap()
{
this.Table("category");
Id(x => x.Id).Column("id").GeneratedBy.Native();
Map(x => x.Name).Column("name");
References(x => x.Shop);
HasManyToMany(x => x.Articles).Cascade.AllDeleteOrphan()
.Table("article_category")
.ChildKeyColumn("article_id")
.ParentKeyColumn("category_id")
.Inverse();
}
}
public class ArticleMap: ClassMap<Article>
{
public ArticleMap()
{
this.Table("article");
Id(x => x.Id).Column("id").GeneratedBy.Native();
Map(x => x.Name).Column("name");
References(x => x.Shop);
HasManyToMany(x => x.Categories).Cascade.All()
.Table("article_category")
.ParentKeyColumn("article_id")
.ChildKeyColumn("category_id");
}
}
When deleting a Category (Session.Delete()), NH tries to delete the related Articles as well. Changing the Cascade-Mode to SaveUpdate will fix this, but will leave the entries in the link table *article_category*. Summing up : Cascade.SaveUpdate is too lazy, Cascade.All is too eager.
I tried everything that came to my mind in the mappings, but couldn't find a correct way to map this (rather simple schema).
Any ideas on how to (fluently) map this are greatly appreciated!
Thanks in advance
Sebi
The entries are left in the link table because Category.Articles is defined as the inverse side of the relationship. You need to remove the Category from Article.Categories before deleting it in order for the link record to be removed.