i have this class that maps a tree of Tag objects :
public class Tag
{
public int Id { get; set; }
public int Description { get; set; }
private IList<Tag> childTag = new List<Tag>();
public virtual IEnumerable<Tag> ChildTag
{ get { return childTag.ToArray(); } }
public virtual void Add(Tag child)
{ childTag .Add(child); }
public virtual bool Remove(Tag child)
{ return childTag .Remove(child); }
}
with this map:
public TagMap()
{
Id(x => x.Id)
.Column("Kint_T01_IdTag")
.GeneratedBy.Assigned();
HasMany(x => x.ChildTag)
.KeyColumn("ParentId")
.Cascade.All()
.Access.CamelCaseField().ReadOnly();
}
when i run this test
Tag Tag= fixture.Build<Tag>().Do(x => x.Add(fixture.Create<Tag>())).Create();
Tag TagActual;
using (IUnitOfWork uow = new UnitOfWork())
{
uow.openConnection();
uow.BeginTran();
uow.TagRepository.Create(Tag);
uow.commit();
Tag.Rmove(Tag.ChildTag.First());
uow.TagRepository.Update(Tag);
uow.commit();
uow.closeConnection();
uow.openConnection();
TagActual = uow.TagRepository.GetById(Tag.Id);
TagActual.ShouldBeEquivalentTo(Tag);
uow.closeConnection();
}
it fails because TagActual.ChildTag contains a child even if i remove it from parent collection.
debugging the test i see that after
uow.TagRepository.Create(Tag);
uow.commit();
two record are isnerted in table and parent property of children is set correctly
Tag.Rmove(Tag.ChildTag.First()); work correctly and after that the collection is empty
but after
uow.TagRepository.Update(Tag);
uow.commit();
nothing change in table and 'parent' filed is still set.
In this way the test failed because TagActual has a collection of children while this should be empty
Why NHibernate correctly manage the add operation but not the remove
Is there a way to do it without adding Parent property to my object and manually manage it?
I find it :)
i done 2 mistakes:
.ReadOnly(); in map, it's useless
forgot beginTrans in test case
test must be written so
Tag Tag= fixture.Build<Tag>().Do(x => x.Add(fixture.Create<Tag>())).Create();
Tag TagActual;
using (IUnitOfWork uow = new UnitOfWork())
{
uow.openConnection();
uow.BeginTran();
uow.TagRepository.Create(Tag);
uow.commit();
uow.BeginTran();
Tag.Rmove(Tag.ChildTag.First());
uow.TagRepository.Update(Tag);
uow.commit();
uow.closeConnection();
uow.openConnection();
TagActual = uow.TagRepository.GetById(Tag.Id);
TagActual.ShouldBeEquivalentTo(Tag);
uow.closeConnection();
}
This is how NHibernate is designed, according to http://nhibernate.info/doc/nh/en/index.html#example-parentchild
Basically the solution is in adding an extra property "Parent" which is responsible for the relationship:
public class Tag
{
public int Id { get; set; }
public int Description { get; private set; }
private IList<Tag> childTag = new List<Tag>();
private Tag parentTag;
public virtual IEnumerable<Tag> ChildTag { get { return childTag.ToArray(); } }
public virtual void Add(Tag child) { childTag .Add(child); }
public virtual bool Remove(Tag child) { return childTag .Remove(child); }
}
with this map:
public TagMap()
{
Id(x => x.Id)
.Column("Kint_T01_IdTag")
.GeneratedBy.Assigned();
Reference(x => x.ParentTag).Access.CamelCaseField();
HasMany(x => x.ChildTag)
.Inverse()
.KeyColumn("ParentId")
.Cascade.AllDeleteOrphan()
.Access.CamelCaseField().ReadOnly();
}
Now everything should work as expected...
Related
I am trying to update an nHibernate object with a child collection using the .Update() method found on a hibernate session. The only thing that I can do with the current setup is add children, I can not modify them or remove them.
For clarification the objects and their mapping are as follows:
public class Parent {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual ISet<Child> Children { get; set; } = new HashSet<Child>();
}
public class ParentMap: ClassMap<Parent>
{
public ParentMap()
{
Id(x => x.Id);
Map(x => x.Name);
HasMany(x => x.Children)
.AsSet()
.Inverse()
.Cascade.AllDeleteOrphan();
}
}
public class Child {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Parent Parent { get; set; }
}
public class ChildMap: ClassMap<Child>
{
public ChildMap()
{
Id(x => x.Id);
Map(x => x.Name);
References(x => x.Parent);
}
}
When I get changes from my UI layer and try to update the already existing object using:
using (var tx = _session.BeginTransaction())
_session.Update(newParent);
tx.Commit();
}
Here newParent is a transient object (obtained from the database in an earlier session andd shown in the UI) containing the same identifier as the object I would like to update, but with changes to the child collection. Somehow using this approach I can only add children, but not modify or remove them.
Where is my mistake?
Most likely what is happening to you, is that instead of modifying the set that NHibernate has instantiated in your Parent entity, you are replacing it all together by a new instance of HashSet.
When you save or get an entity from NHibernate, your Children ISet gets loaded with an instance of a PersistentGenericSet (that implements ISet) which has the responsibility of helping with this change tracking for your collection.
In short, do not assign to the Children property. In fact, make the setter protected.
Just Add() or Remove() or Clear() it as required.
My question is, is there a possible Fluent NHibernate mapping for Parent and Child objects that does not require the Child object to have a Parent object property? I haven't figured out how to map the reference back to the Parent. When I call Create with the mappings as-is, I get an exception because the Child object does not have the required foreign key (required in the data store) back to the Parent.
I have two POCO classes:
public class Parent
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Child> Childs { get; set; }
}
public class Child
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual int ParentId { get; set; }
}
And some mappings:
public class ParentMap : ClassMap<Parent>
{
public ParentMap()
{
this.Table("Parents");
this.Id(x => x.Id);
this.Map(x => x.Name);
this.HasMany(x => x.Childs).KeyColumn("ChildId").Cascade.AllDeleteOrphan();
}
}
public class ChildMap : ClassMap<Child>
{
public ChildMap()
{
this.Table("Childs");
this.Id(x => x.Id);
this.Map(x => x.Name);
// Needs some sort of mapping back to the Parent for "Child.ParentId"
}
}
And Create method:
public Parent Create(Parent t)
{
using (this.session.BeginTransaction())
{
this.session.Save(t);
this.session.Transaction.Commit();
}
return t;
}
I want to be able to create a Parent object that has a list of Child objects, but not have the Child objects have references back to their Parent (other than the Parent ID). I want to do this to avoid the circular reference from Parent to a list of Childs back to the Parent object, since that is causing issues with JSON serialization.
You can map these entities without problem, try this:
public class ParentMap : ClassMap<Parent>
{
public ParentMap()
{
this.Table("Parents");
this.Id(x => x.Id);
this.Map(x => x.Name);
this.HasMany(x => x.Childs).KeyColumn("ChildId").Cascade.AllDeleteOrphan();
}
}
public class ChildMap : ClassMap<Child>
{
public ChildMap()
{
this.Table("Childs");
this.Id(x => x.Id);
this.Map(x => x.Name);
this.Map(x => x.ParentId);
// if you have a reference of Parent object, you could map as a reference, for sample:
this.References(x => x.Parent).Column("ParentId");
}
}
When you get entities from ISession, do not serialize it to some format because these can be proxies of nhibernate instead entities objects. Try to create DTO (Data Transfer Object) classes and convert these entities to a DTO object, and serialize it. You will avoid circular references. For sample:
public class ParentDTO
{
public int Id { get; set; }
public string Name { get; set; }
public int ParentId { get; set; }
/* here you just have the primitive types or other DTOs,
do not reference any Entity type*/
}
And when you need to read the values to share the serialized value:
var dto = ISession.Query<Parent>()
.Select(x =>
new ParentDTO() {
Id = x.Id,
Name = x.Name,
ParentId = x.ParentId)
.ToList();
Get this result from the Data Access Layer and try to serialize, for sample:
var result = Serialize(dto);
I have the following model
public class BaseEntity
{
public virtual int EntityID { get;set; }
public virtual bool Active { get;set; }
}
public class Brand : BaseEntity
{
public Brand()
{
Series = new List<Series>();
}
public virtual string BrandName { get;set; }
public virtual string Website { get;set; }
public virtual IList<Series> Series;
}
public class Series: BaseEntity
{
public Series()
{
Products = new List<Product>();
}
public virtual Brand Brand { get;set; }
public virtual string SeriesName { get;set; }
public virtual string SeriesDescription { get;set; }
public virtual IList<Product> Products;
}
public class Product : BaseEntity
{
public Product()
{
Series = new List<Series>();
}
public virtual Series Series { get;set; };
public virtual string ProductName { get;set; }
public virtual string ProductCode { get;set; }
public virtual string ProductDescription { get;set; }
public virtual double SellingPrice { get;set; }
}
I have set all mappings to not lazy load.
public class BrandMapping() : ClassMap<Brand>
{
public BrandMapping()
{
Table("Brand");
Id(item => item.EntityID).Column("BrandID").GeneratedBy.Increment();
Map(item => item.BrandName).Not.LazyLoad();
Map(item => item.Active).Not.LazyLoad();
Map(item => item.Website).Not.LazyLoad();
HasMany<Series>(item => item.Series).Cascade.All().Not.LazyLoad();
}
}
public class SeriesMapping() : ClassMap<Series>
{
public SeriesMapping()
{
Table("Series");
Id(item => item.EntityID).Column("SeriesID").GeneratedBy.Increment();
Map(item => item.SeriesName).Not.LazyLoad();
Map(item => item.SeriesDescription).Not.LazyLoad();
Map(item => item.Active).Not.LazyLoad();
HasMany<Product>(item => item.Products).Cascade.All().Not.LazyLoad();
Reference<Brand>(item => item.Brand).Column("BrandID");
}
}
public class ProductMapping() : ClassMap<Product>
{
public ProductMapping()
{
Table("Product");
Id(item => item.EntityID).Column("ProductID").GeneratedBy.Increment();
Map(item => item.ProductName).Not.LazyLoad();
Map(item => item.ProductDescription).Not.LazyLoad();
Map(item => item.ProductCode).Not.LazyLoad();
Map(item => item.SellingPrice).Not.LazyLoad();
Map(item => item.Active).Not.LazyLoad();
Reference<Series>(item => item.Series).Column("ProductID");
}
}
I have a generic Repository base that has the following code for the Load method so I don't need to create code for each typed Repo:
IEnumerable<T> Load()
{
IList<T> results = new List<T>();
StartTransaction();
results = _session.CreateCriteria(typeof(T)).List<T>();
CommitTransaction();
return results;
}
My issue is that one of my screens displays products. This screen needs to display the Series and Brand names for completeness in the grid. I have found that even with lazy loading disabled, the Series is not even loaded, never mind the Brand record. I need to find a way to add the SetFetchMode to the above load code to ensure that all relationship trees (Product->Series->Brand) are loaded when a record is loaded from the DB.
Anyone got ideas how I can do generic SetFetchmode?
It would be easiest to specify eager loading in the mapping files for each entity, this is done by adding the Fetch specification.
If you are using Fluent nHibernate then this should do it:
References(x => x.Series).Column("SeriesId").ForeignKey("Id").Fetch.Join();
If you put this inside your Product mapping it tells nHiberate to eager load the Series whenever a Product is loaded.
Likewise put something similar in your Series to load the Brand.
I have an issue when deleting a child record from the session. Here are the entities i have defined:
public class ReportedData
{
public virtual int ReportedDataID { get; set; }
public virtual ReportedDataTypes Type { get; set; }
public virtual string Reason { get; set; }
public virtual IList<ArticleCommentReported> ArticleCommentsReported { get; private set; }
public virtual IList<ForumPostReported> ForumPostsReported { get; private set; }
public ReportedData()
{
ArticleCommentsReported = new List<ArticleCommentReported>();
ForumPostsReported = new List<ForumPostReported>();
}
}
public class ArticleCommentReported : ReportedData
{
public virtual ArticleComment Comment { get; set; }
}
public class ForumPostReported : ReportedData
{
public virtual ForumPost Post { get; set; }
}
With the following fluent mapping:
public ReportedDataMap()
{
Table("ReportedData");
Id(x => x.ReportedDataID);
Map(x => x.Type, "TypeID");
Map(x => x.Reason);
HasMany(x => x.ArticleCommentsReported)
.KeyColumn("ReportedDataID")
.Inverse()
.Cascade.All();
HasMany(x => x.ForumPostsReported)
.KeyColumn("ReportedDataID")
.Inverse()
.Cascade.All();
}
public class ArticleCommentReportedMap : SubclassMap<ArticleCommentReported>
{
public ArticleCommentReportedMap()
{
Table("ArticleCommentsReported");
KeyColumn("ReportedDataID");
References(x => x.Comment, "CommentID");
}
}
public class ForumPostReportedMap : SubclassMap<ForumPostReported>
{
public ForumPostReportedMap()
{
Table("ForumPostsReported");
KeyColumn("ReportedDataID");
References(x => x.Post, "PostID");
}
}
Now say i try the following (i have added comments to help you understand what's going on):
// Loop over the reported data (this is my view model and not my actual model which contains an extra property for the action they wish to carry out)
foreach (var reportedData in model)
{
// If the action is leave then do nothing (else we always delete the reported data)
if (reportedData.Action != ReportedDataActions.Leave)
{
// Switch over the type since we need to make sure it deletes the article comment or post if the action is set to delete
switch (reportedData.Type)
{
case ReportedDataTypes.ArticleComment:
var reportedComment = _context.Repository<ArticleCommentReported>().GetByID(reportedData.ReportedDataID);
if (reportedData.Action == ReportedDataActions.Delete)
_context.Repository<ArticleComment>().Delete(reportedComment.Comment);
_context.Repository<ArticleCommentReported>().Delete(reportedComment);
break;
case ReportedDataTypes.ForumPost:
var reportedPost = _context.Repository<ForumPostReported>().GetByID(reportedData.ReportedDataID);
if (reportedData.Action == ReportedDataActions.Delete)
_forumService.DeletePost(reportedPost.Post);
_context.Repository<ForumPostReported>().Delete(reportedPost);
break;
}
}
}
_context.Commit();
When the user trys to delete a forum post (the Action against the reported data is set to delete) it throws the following error:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [ForumPostReported#2]
I could probably set some mapping up to automatically delete the post/comment once the reported data is deleted but i only want to delete the post/comment if the action is set to delete.
I'd appreciate it if someone could help. Thanks
Problem solved! I just had to delete the reported item first. Seems kinda backwards but it works and that's all i care.
I need help in creating the correct fluent nh mapping for this kind of scenario:
A category can be a child of one or more categories. Thus, resulting to this entity:
public class Category : Entity, IAggregateRoot
{
[EntitySignature]
public virtual string Name { get; set; }
public virtual IList<Category> Parents { get; set; }
public virtual IList<Category> Children { get; set; }
public virtual IList<ProductCategory> Products { get; set; }
public Category()
{
Parents = new List<Category>();
Children = new List<Category>();
Products = new List<ProductCategory>();
}
public virtual void AddCategoryAsParent(Category parent)
{
if (parent != this && !parent.Parents.Contains(this) && !Parents.Contains(parent))
{
Parents.Add(parent);
parent.AddCategoryAsChild(this);
}
}
public virtual void RemoveCategoryAsParent(Category parent)
{
if (Parents.Contains(parent))
{
Parents.Remove(parent);
parent.RemoveCategoryAsChild(this);
}
}
public virtual void AddCategoryAsChild(Category child)
{
if(child != this && !child.Children.Contains(this) && !Children.Contains(child))
{
Children.Add(child);
child.AddCategoryAsParent(this);
}
}
public virtual void RemoveCategoryAsChild(Category child)
{
if(Children.Contains(child))
{
Children.Remove(child);
child.RemoveCategoryAsParent(this);
}
}
}
My initial mapping is this:
public class CategoryMap : ClassMap<Category>
{
public CategoryMap()
{
Id(p => p.Id).GeneratedBy.Identity();
HasManyToMany(x => x.Parents)
.Table("CategoryParents")
.ParentKeyColumn("CategoryId")
.ChildKeyColumn("ParentCategoryId")
.Cascade.SaveUpdate()
.LazyLoad()
.AsBag();
HasManyToMany(x => x.Children)
.Table("CategoryParents")
.ParentKeyColumn("ParentCategoryId")
.ChildKeyColumn("CategoryId")
.Cascade.SaveUpdate()
.Inverse()
.LazyLoad()
.AsBag();
}
}
The problem with this mapping is whenever I remove a category as a parent or as a child of another category, the resulting SQL statement is this:
NHibernate: DELETE FROM CategoryParents WHERE CategoryId = #p0;#p0 = 2
NHibernate: INSERT INTO CategoryParents (CategoryId, ParentCategoryId) VALUES (#p0, #p1);#p0 = 2, #p1 = 3
It deletes all the mapping first, then insert the remaining mapping. The proper way is just delete the category parent mapping which this kind of statement:
DELETE FROM CategoryParents WHERE CategoryId = #p0 AND ParentCategoryId = #p1;#p0 = 2, #p1=1
Any ideas?
after looking at your mappings I think you are wanting to change your Cascade options. Here is an article which outlines a parent - child relationship between entities. Although it comes from the point of view of having orphaned entities I think you will find the blog helpful. Good luck...
http://ayende.com/Blog/archive/2006/12/02/nhibernatecascadesthedifferentbetweenallalldeleteorphansandsaveupdate.aspx