nhibernate "cascade="all-delete-orphan" error - c#

i have 3 tables in my database:
Projects (id, name)
Tags (id, name)
ProjectsTagss (id, projectId, tagid)
As you can see the ProjectsTags table is a bridge table
here is my fluent nhibernate mapping
ProjectMap.cs:
Map(x => x.Name).Not.Nullable();
HasMany(x => x.ProjectsTags).AsBag().Inverse()
.Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);
ProjectsTagsMap.cs:
References(x => x.Project).Not.Nullable();
References(x => x.Tag).Not.Nullable();
TagMap.cs:
Map(x => x.Name).Not.Nullable();
As you can see, i historically didn't have the Tag table linked to anything else. I now need to generate a report to show Tag and how often that tag is used so i need to join from Tag to ProjectsTag. i tried adding this line into the tagsmap:
HasMany(x => x.ProjectsTags).AsBag().Inverse()
.Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);
but when i go to update the name on a tag object and commit, i get this error:
A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance
can anyone see anything wrong with what i added that would be causing this nhibernate exception when i simply update the Tag table. Again my goal is to be able to do something like:
Tag.ProjectTags.Count();
Here is some additional code as requested:
my Tag Class:
public class Tag
{
public virtual IList<ProjectTag> ProjectTags { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
}

Somewhere in your code, you should have dereferenced the original collection on your Project domain. I suspect that your code goes like this:
var project = Session.Get<Project>();
project.ProjectsTags = new List<ProjectsTags> { someProjectsTagsInstance };
Session.Save(project);
If this is the case, you should do this instead:
var project = Session.Get<Project>();
project.ProjectsTags.Clear();
project.ProjectsTags.Add(someProjectsTagsInstance);
Session.Save(project);

While a collection is not modified, NH can still think that it is. Something like this could be caused by a ghost update. From NHibernate 3.0 Cookbook, Jason Dentler (page 184): "As part of automatic dirty checking, NHibernate compares the original state of an entity to
its current state. An otherwise unchanged entity may be updated unnecessarily because a
type conversion caused this comparison to fail".
Ghost update of collection can be caused by code that looks like this:
public class Tag
{
private IList<ProjectTag> projectsTags;
public virtual IEnumerable<ProjectTag> ProjectsTags
{
get
{
return new ReadOnlyCollection<ProjectTag>(projectsTags);
}
set
{
projectsTags = (IList<ProjectTag>)value;
}
}
}
ProjectsTags property returns the collection in readonly wrapper, so client code cannot add or remove elements to/from the collection.
The error will appear even when name of a tag is not changed:
private void GhostTagUpdate(int id)
{
using (var session = OpenSession())
{
using (var transaction = session.BeginTransaction())
{
var tag = session.Get<Tag>(id);
transaction.Commit();
}
}
}
ProjectsTags collection should be mapped with CamelCaseField access strategy to avoid ghost updated:
HasMany(x => x.ProjectsTags)
.Access.CamelCaseField()
.AsBag().Inverse().Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);
Anyway...
Your association seems to be diabolically complex. If ProjectsTags table should contains only id of tag and id of project, then it would be simpler to use FNH many-to-many bidirectional mapping:
public class Tag2Map : ClassMap<Tag2>
{
public Tag2Map()
{
Id(x => x.Id);
Map(x => x.Name);
HasManyToMany(x => x.Projects)
.AsBag()
.Cascade.None()
.Table("ProjectsTags")
.ParentKeyColumn("TagId")
.ChildKeyColumn("ProjectId");
}
}
public class Project2Map : ClassMap<Project2>
{
public Project2Map()
{
Id(x => x.Id);
Map(x => x.Name);
HasManyToMany(x => x.Tags)
.AsBag()
.Cascade.None()
.Inverse()
.Table("ProjectsTags")
.ParentKeyColumn("ProjectId")
.ChildKeyColumn("TagId");
}
}
Now there is no need for ProjectTag entity in the model. The count of how many times is given tag used can be retrieved in two ways:
Direct way: tag.Projects.Count() - but it retrieves all projects from database.
Query way:
var tag = session.Get<Tag2>(tagId);
var count = session.Query<Project2>().Where(x => x.Tags.Contains(tag)).Count();

Related

EF Core 6 Database First Parent Child Relation issue

We are building an application using .NET 6 and EF Core 6 with an existing SQL Server database. We are using the database first approach and running the Scaffold-DbContext tool we were able to generate the dbcontex class. Everything works fine, a part for a parent child relation between two tables:
The scaffold tool, for the above tables generated the following two classes:
public partial class TreeNode
{
public TreeNode()
{
TreeNodeHierarchyChildren = new HashSet<TreeNodeHierarchy>();
TreeNodeHierarchyParents = new HashSet<TreeNodeHierarchy>();
}
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public bool IsLeaf { get; set; }
public int? OrganisationId { get; set; }
public bool IsDeleted { get; set; }
public virtual ICollection<TreeNodeHierarchy> TreeNodeHierarchyChildren { get; set; }
public virtual ICollection<TreeNodeHierarchy> TreeNodeHierarchyParents { get; set; }
}
public partial class TreeNodeHierarchy
{
public int Id { get; set; }
public int ParentId { get; set; }
public int ChildId { get; set; }
public virtual TreeNode Child { get; set; }
public virtual TreeNode Parent { get; set; }
}
And in the dbcontext class the following mapping:
modelBuilder.Entity<TreeNode>(entity =>
{
entity.ToTable("TreeNode");
entity.Property(e => e.Code).HasMaxLength(100);
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(255);
});
modelBuilder.Entity<TreeNodeHierarchy>(entity =>
{
entity.ToTable("TreeNodeHierarchy");
entity.HasOne(d => d.Child)
.WithMany(p => p.TreeNodeHierarchyChildren)
.HasForeignKey(d => d.ChildId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Child");
entity.HasOne(d => d.Parent)
.WithMany(p => p.TreeNodeHierarchyParents)
.HasForeignKey(d => d.ParentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Parent");
});
Here is the issue, when I write the following:
var nodes = _context.TreeNodes.Include(th => th.TreeNodeHierarchyChildren)
.Where(tn => tn.IsLeaf)
.....
it loads the child but not the parent.
This relation works properly in the current application (.net 4.7) using LINQ to SQL.
Am I missing something?
Updated
as suggested from #SpruceMoose, I included also the TreeNodeHierarchyParents property in the query but it didn't fix the issue.
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
Updated #2
I applied the mapping suggested from #Dave which in my opinion it makes sense (at the end the relation is like the Windows folders/files system).
Anyway there is still something that's not working properly. When I debug the following code:
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
.ToList();
I still see that the parent has not been loaded
Updated #3
I applied the change to the query as suggested from #Moho
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.ThenInclude(tnhc => tnhc.Child)
.Include(th => th.TreeNodeHierarchyParents)
.ThenInclude(tnhp => tnhp.Parent)
.Where(tn => tn.IsLeaf)
.ToList();
and finally we got the Parent value
Now we are missing the last step, the parents of a parent
You need to explicitly (eagerly) load the Parent elements by using an Include() on the TreeNodeHierarchyParents navigation property (as you are currently for the TreeNodeHierarchyChildren navigation property).
Change your linq query to the following:
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
.....
I think your relationship mapping is wrong. You say one child has many children and one parent has many parents. It should be one child has many parents, and one parent has many children.
I think it's also a good idea to define these kinds of relationships on both sides, so that if you get something wrong it shows up as an error faster. Note also that I think some of these statements would already be the default.
Also, important, note that I think you need to use Nullable Reference Types to indicate nullability. Anything that is supposed to be nullable should have a ? on its type name in the entity types. Though I think it's possible you should cascade delete, not set null. It depends how your model works.
Something like this, though I can't guarantee compilation:
modelBuilder.Entity<TreeNode>(tnb => {
tnb.ToTable("TreeNode");
tnb.Property(tn => tn.Code).HasMaxLength(100);
tnb.Property(tn => tn.Name).IsRequired().HasMaxLength(255);
tnb
.HasMany(tn => tn.TreeNodeHierarchyParents)
.WithOne(tnh => tnh.Child);
tnb
.HasMany(tn => tn.TreeNodeHierarchyChildren)
.WithOne(tnh => tnh.Parent);
});
modelBuilder.Entity<TreeNodeHierarchy>(tnhb => {
tnhb.ToTable("TreeNodeHierarchy");
tnhb
.HasOne(tnh => tnh.Child)
.WithMany(tn => tn.TreeNodeHierarchyParents)
.HasForeignKey(tnh => tnh.ChildId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Child");
tnhb
.HasOne(tnh => tnh.Parent)
.WithMany(tn => tn.TreeNodeHierarchyChildren)
.HasForeignKey(tnh => tnh.ParentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Parent");
});
One thing you can do to try to make sure your model definition is correct is to create an empty second database with it and compare its model against the real one, and then keep fine tuning it until you get it right.
Regarding Update #2:
You’re eager loading the relationship entities of type TreeNodeHierarchy but you are not eager loading the TreeNode entities they reference. You need to add .ThenInclude calls to do so.
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.ThenInclude(tnhc => tnhc.Child)
.Include(th => th.TreeNodeHierarchyParents)
.ThenInclude(tnhp => tnhp.Parent)
.Where(tn => tn.IsLeaf)
The Child property in your example is populated in your current query because the TreeNode entity is loaded by your base query and EF Core will automatically hook it up to relevant navigation properties in other tracked entities. Thus, any “parent” that is not IsLeaf (nor any “child” that is not IsLeaf) is not loaded without the additional ThenInclude.
Another (not recommended) alternative is to enable lazy loading.

Include causes OutOfMemoryException

I have a very strange problem when using Entity Framework with Code First approach.
I have disabled lazy loading and are using include statements when I need.
Unfortunately I cannot show you the code, but will try to explain as detailed I can.
I have two tables where one is referring to the other:
TableA (ID, Name)
TableB (ID, ForeignKeyTableA)
The corresponding classes:
public class A
{
public int ID { get; set; }
public string Name { get; set; }
}
public class B
{
public int ID { get; set; }
public A ClassA { get; set; }
}
I can load A and B by themselves without and problem, but when I write for example:
Include(p => p.A) when loading B and finally calling ToList() it will cause an OutOfMemoryException after a while.
The tables aren't that big. Table A have around 600 records and table B 5 records.
The mapping for class A looks like this:
this.ToTable("TableA");
this.HasKey(p => p.ID);
this.Property(p => p.ID).HasColumnName("ID");
this.Property(p => p.ID).HasColumnName("Name");
The mapping for class B looks like this:
this.ToTable("TableB");
this.HasKey(p => p.ID);
this.Property(p => p.ID).HasColumnName("ID");
HasRequired(p => p.A).WithOptional()
.Map(m => m.MapKey("ForeignKeyTableA"));
I need to map them because the column and table names aren't by convention. Include can be used on other objects without any problems.
The weirdest thing is that when I am using Include() the database will never be hit.
Anyone got suggestions? Because I am really running out of ideas now.

Many-to-One with fixed values for a missing column

I'm dealing with a legacy database that has a locked schema. The problem I'm facing is that a number of the tables are keyed off known/fixed/hard-coded entity type Id values instead of having a column values. This means that I can't use the normal References construct.
For the tables with the ENTITY_TYPEID I can do this:
public class EntityMap : ClassMap<Entity>
{
public EntityMap()
{
References(x => x.Type)
.Columns("ENTITY_SECTION","ENTITY_TYPEID");
}
}
which happily populates the Entity.Type.
For the tables of a fixed/known/hard-coded type I need to map to a hard-coded value instead of the ENTITY_TYPE column, so to para-phrase in code:
public class EntityXMap : ClassMap<EntityX>
{
public EntityXMap(int entityType)
{
References(x => x.Type)
.Columns("ENTITY_SECTION", "ENTITY_TYPE = 123" );
}
}
HasMany() has a Where() construct that I could use in that case...
Any ideas how to achieve something similar here?
maybe overkill but you could try
// add to config
var typemap = new TypeMap();
typemap.Id(x => x.Section, "ENTITY_SECTION");
typemap.Where("ENTITY_TYPE = 123");
typemap.EntityName("Type for EntityX");
References(x => x.Type)
.Column("ENTITY_SECTION")
.EntityName("Type for EntityX");

Fluent Nhibernate Aggregate objects mapping

Say I have the following class with an aggregation of an external class:
public class MyMovie
{
public virtual string id{get;set;}
public virtual Movie movie{get;set;}
}
//These classes are externally defined and cannot be changed.
public class Movie
{
public string title{get;set;}
public IList<Director> Directors{get;set;}
}
public class Director
{
public string name{get;set;}
public IList<Movie> DirectedMovies{get;set;}
}
The db schema for this would be three tables:
Movie(m_id, title)
Director(d_id, name)
Directs(m_id, d_id)
Is it possible to map this with fluent nhibernate? I just don't understand how this would be done with the many to many relation being in the external classes where I cannot map create a map class for Director as this does not define members as virtual.
Map your class MyMovie as usual, and use disable lazyloading of Movie and Director. Aftter all lazy-loading for many-to-many part should work as usualy, cause for collection laziness proxy is not need.
public class MyMovieMap : ClassMap<MyMovie>
{
public MyMovieMap()
{
Id(x => x.id);
References(x => x.movie);
}
}
public class MovieMap : ClassMap<Movie>
{
public MovieMap()
{
Not.LazyLoad();
Id<int>("m_id");
Map(x => x.title);
HasManyToMany(x => x.Directors)
.Table("Directs")
.LazyLoad();
}
}
public class DirectorMap : ClassMap<Director>
{
public DirectorMap()
{
Not.LazyLoad();
Id<int>("d_id");
Map(x => x.name);
HasManyToMany(x => x.DirectedMovies)
.Table("Directs")
.LazyLoad();
}
}
Basically, your issue here is that you are trying to tell nhibernate to load objects, but it doesn't know anything about the objects. For instance, you are telling it MyMovie contains a Movie, yet it doesn't know what field Movie.title belongs to, and it doesn't know how to join in Director's with the movies because it is unmapped. So basically in order to pull this off without a mapping file, you need to use Criteria and result transformers to accomplish this (basically issuing a sql query and converting the results to objects via an on the fly mapping), you could encapsulate this logic in a function so it can be called in your code without being too messy, but other than that I can't see any other way around it. Check out this post, the code is not exactly what you are trying to do (because you will have to join in directors), but it is using the same tools you will have to use... http://ayende.com/blog/2741/partial-object-queries-with-nhibernate

How to Specify Columntype in fluent nHibernate?

I have a class CaptionItem
public class CaptionItem
{
public virtual int SystemId { get; set; }
public virtual int Version { get; set; }
protected internal virtual IDictionary<string, string> CaptionValues {get; private set;}
}
I am using following code for nHibernate mapping
Id(x => x.SystemId);
Version(x => x.Version);
Cache.ReadWrite().IncludeAll();
HasMany(x => x.CaptionValues)
.KeyColumn("CaptionItem_Id")
.AsMap<string>(idx => idx.Column("CaptionSet_Name"), elem => elem.Column("Text"))
.Not.LazyLoad()
.Cascade.Delete()
.Table("CaptionValue")
.Cache.ReadWrite().IncludeAll();
So in database two tables get created. One CaptionValue and other CaptionItem. In CaptionItem table has three columns
1. CaptionItem_Id int
2. Text nvarchar(255)
3. CaptionSet_Name nvarchar(255)
Now, my question is how can I make the columnt type of Text to nvarchar(max)?
Thanks in advance.
I tried many things, but nothing was solving the problem. Eventually I solved it. I just changed a bit in the mapping code. The new mapping code is given below
Id(x => x.SystemId);
Version(x => x.Version);
Cache.ReadWrite().IncludeAll();
HasMany(x => x.CaptionValues)
.KeyColumn("CaptionItem_Id")
.AsMap<string>(idx => idx.Column("CaptionSet_Name"), elem => elem.Columns.Add("Text", c => c.Length(10000)))
.Not.LazyLoad()
.Cascade.Delete()
.Table("CaptionValue")
.Cache.ReadWrite().IncludeAll();
So I just added c => c.Length(10000). Now, in the database the columntype of Text will be nvarchar(MAX).
Hope it will help anybody else.
You can do that, using the following:
Id(x => x.Text).CustomSqlType("nvarchar(max)");
// For older versions of Fluent NHibernate
Id(x => x.Text).CustomSqlTypeIs("nvarchar(max)");
Edit:
Bipul, Why not create a mapping for the CaptionValues and in that mapping, specify that, the type of Text is nvarchar(max) ?

Categories

Resources