I am writing a small bookmarking tool in ASP.NET MVC, and each bookmark is stored in a folder. I am currently representing this by simply storing a path as a string in the database, but when it comes to beginning to display items by folder and list folders this seems like an efficient way to do it.
Is there any built-in way to represent and then manipulate paths in a database? Or an accepted best practice?
If you are talking paths like in filesystem paths, then you might want to start thinking about about a trees and a hierachical system.
In essence, you will have an additional table in a form named like Category or something, which is self referencing with a ParentId. With this you can iterate along your caegories until, there is no parent or child item, depending on the direction of your query and build a path with the result of your query.
This of course can get ugly, if your bookmarkings have more then 1 category. You will want to get some additional validation into this.
The following code is acutally from an article on the web, which I used myself, but I can't find the article again, to quote it as the source.
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public int? ParentId { get; set; }
public virtual Category Parent { get; set; }
public virtual ICollection<Category> Children { get; set; }
}
public class CategoryMapping : EntityTypeConfiguration<Category>
{
public CategoryMapping()
{
HasKey(x => x.CategoryId);
Property(x => x.CategoryId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(x => x.Name)
.IsRequired()
.HasMaxLength(255);
//Parent is optional, because root node has no parent
HasOptional(x => x.Parent)
.WithMany(x => x.Children)
.HasForeignKey(x => x.ParentId)
.WillCascadeOnDelete(false);
}
}
References:
http://sqllessons.com/categories.html
http://www.sitepoint.com/hierarchical-data-database/
Entity Framework Code First Self Referencing Parent Child with Payload
Related
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.
I want my Donut to optionally relate to another Donut, and if so, the other donut would relate back. From what I've read, I believe I need to set this up as a Parent/Child relationship, even though in the real world, it's just an optional pairing (Donuts can exist by themselves happily, or exist in pairs). Here is my current setup:
public class Donut {
public int Id { get; set; }
public int? ParentDonutId { get; set; }
public int? ChildDonutId { get; set; }
// virtual properties
public virtual Donut ParentDonut { get; set; }
public virtual Donut ChildDonut { get; set; }
}
This statement in my mapping file gets me close, but insists on creating a new key named ParentDonut_Id on the table instead of using my existing ParentDonutId:
this.HasOptional(t => t.ChildDonut)
.WithOptionalPrincipal(t => t.ParentDonut);
But when I try this mapping:
this.HasOptional(t => t.ChildDonut)
.WithOptionalPrincipal(t => t.ParentDonut)
.Map(m => m.MapKey("ParentDonutId")); // or "ChildDonutId"
I get this error message when trying to Add-Migration:
ParentDonutId: Name: Each property name in a type must be unique. Property name 'ParentDonutId' is already defined.
How should I set up this relationship? Is it possible? It seems logical enough to me, but maybe it's one of these things DB's don't let you do?
EDIT: I came across this hack, which will probably allow me to do what I need, but it doesn't allow navigating backwards from a child to a parent Donut, which would be nice to have:
this.HasOptional(t => t.ChildDonut)
.WithMany() // haxx
.HasForeignKey(t => t.ChildDonutId);
What if you map both sides? I think this should work:
modelBuilder.Entity<Donut>()
.HasOptional(e => e.ChildDonut)
.WithMany()
.HasForeignKey(t => t.ChildDonutId);
modelBuilder.Entity<Donut>()
.HasOptional(e => e.ParentDonut)
.WithMany()
.WithMany(t => t.ParentDonutId);
To understand more about self-referencing take a look at this thread Second Self-To-Self relationship in Entity Framework
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.
Using Entity Framework (Code First), I'm trying to map a conditional/filtered relationship between the following 2 entities:
Building
BuildingId
BuildingName
Area
AreaId
ParentId
AreaName
IsSubArea
A Building can have many Areas
An Area can have many (Sub)Areas
I would like to create the relationship between Building and Area where the areas marked with 'IsSubArea' are filtered out of the relationship. In this context, ParentId would relate to a Building, otherwise, ParentId would be another Area. This would allow me to create a building with many areas, and each area could have many sub-areas, creating a tree style structure.
The closest to a solution I have found relates to 'soft delete' functionality (source):
modelBuilder.Entity<Foo>().Map(m => m.Requires("IsDeleted").HasValue(false));
Converted to fit my example:
modelBuilder.Entity<Area>().Map(m => m.Requires("IsSubArea").HasValue(false));
But as far as I can tell, this has no bearing on the relationship to the Building.
Another solution would be to create a property on the Building which specifies the query definition to use to return related areas (source):
public class Building
{
public int BuildingId {get; set;}
public string BuildingName {get; set;}
public IQueryable<Area> BuildingAreas
{
get
{
return from area in areas
where area.IsSubArea == false
and area.ParentId == BuildingId
select area;
//Assume I have a reference to relevant DbSets
}
}
}
This solution would work but doesn't feel as elegant as a conditional mapping.
Another solution would be to inherit from Area and create the 2 sub-classes:
BuildingArea
AreaId
BuildingId
AreaName
SubArea
AreaId
ParentAreaId
AreaName
Each inherits from Area and sets the 'IsSubArea' field as appropriate.
This solution feels tidier but I do not know how to implement this in Entity Framework.
Is there a way to specify conditional mapping on relationships?
Is there a better way to implement this structure?
Update 1:Found this & this guide on inheritance which seems to match my requirements. However, neither of these tutorials define relationships between derived types. I'll update the question tonight with what I have tried with regards to the Table per Hierarchy (TPH) method.
Update 2:
I'm going to try an describe the Table per Hierarchy (TPH) method I tried to implement based on the tutorial links above. Forgive me if this gets a little complicated (maybe I'm over thinking it).
Models
The building class remains the same as the OP.
I have created an abstract base class defining the Area properties common to each derived type (BuildingArea and SubArea):
public abstract class Area
{
protected Area(bool isSubArea)
{
IsSubArea = isSubArea;
SubAreas = new List<SubArea>();
}
public int AreaId { get; set; }
public int ParentId { get; set; }
public string AreaName { get; set; }
public bool IsSubArea { get; private set; } //note the private set
public virtual ICollection<SubArea> SubAreas { get; set; }
}
I then have 2 derived types which inherit from Area:
public class BuildingArea : Area
{
public BuildingArea () : base(false)
{}
public virtual Building ParentBuilding { get; set; }
}
public class SubArea : Area
{
public SubArea(): base(true)
{}
// This is of type `Area` because parent could be either `BuildingArea` or `SubArea`
public virtual Area Parent { get; set; }
}
I then have the following 2 EntityTypeConfigurations:
public class BuildingAreaMap : EntityTypeConfiguration<BuildingArea>
{
public BuildingAreaMap ()
{
// Primary Key
HasKey(t => t.AreaId);
// Properties
Property(t => t.AreaName)
.IsRequired()
.HasMaxLength(256);
// Table & Column Mappings
ToTable("Areas");
Property(t => t.AreaId).HasColumnName("AreaId");
Property(t => t.ParentId).HasColumnName("ParentId");
Property(t => t.AreaName).HasColumnName("AreaName");
Property(t => t.IsSubArea).HasColumnName("IsSubArea");
// This is the discriminator column
Map(m => m.Requires("IsSubArea").HasValue(false));
HasRequired(a => a.Site).WithMany(s => s.SiteAreas).HasForeignKey(k => k.ParentId);
}
public class SubAreaMap : EntityTypeConfiguration<SubArea>
{
public SubAreaMap()
{
// Primary Key
HasKey(t => t.AreaId);
// Properties
Property(t => t.AreaName)
.IsRequired()
.HasMaxLength(256);
// Table & Column Mappings
ToTable("AssetHealthAreas");
Property(t => t.AreaId).HasColumnName("AreaId");
Property(t => t.ParentId).HasColumnName("ParentId");
Property(t => t.AreaName).HasColumnName("AreaName");
Property(t => t.IsSubArea).HasColumnName("IsSubArea");
// This is the discriminator column
Map(m => m.Requires("IsSubArea").HasValue(true));
HasRequired(a => a.Parent).WithMany(s => s.SubAreas).HasForeignKey(k => k.ParentId);
}
}
This code builds successfully, but I do get the following runtime error:
Map was called more than once for type 'SiteArea' and at least one of the calls didn't specify the target table name.
But I am specifying the target table name (once in each EntityTypeConfiguration class).
So I removed the EntityTypeConfiguration for SubArea but I get the same error.
One of the tutorials pulls the mapping out of the EntityTypeConfiguration class and puts it in the OnModelCreating handler as follows:
modelBuilder.Entity<Area>()
.Map<BuildingArea>(m => m.Requires("IsSubArea").HasValue(false))
.Map<SubArea>(m => m.Requires("IsSubArea").HasValue(true));
This also gives me the same error.
If I remove relationships from the equation, I get a different error regarding the ParentId property:
The foreign key component 'ParentId' is not a declared property on type 'SiteArea'. Verify that it has not been explicitly excluded from the model and that it is a valid primitive property.
Update 3
An image of the model I'm trying to create...
Update 4
I'm going to try and simplify my model to match the following.
If the solution below works, I will need to have a little more business logic to navigate the tree but it should be manageable.
For the errors when creating the TPH classes:
I think this is because you are not supposed to have the discriminator column as a property in your classes.
Remove the property IsSubArea from your base class.
When you create new instance, EF should automatically detect the type and fill the IsSubArea accordingly.
I am not sure if fluent n hibernate can do this or not, but I cannot figure out how.
I have a table - cases and some properties
ownerId, brokerId, shipperId
I want to map this to my property:
int[] OrgsWithAccess
Is this possible?
This way when I am checking if an org has access to the case, I can check the property OrgsWithAccess rather than OwnerId == myorg.id or brokerId == myorg.id etc.
If I understand your question correctly, I wouldn't recommend trying to map in the way that you have asked.
cases table looks like it is some form of junction table between other tables. I'll assume that these other tables each contain data that are represented as entities in the application, and that there are three tables, Owner, Broker and Shipper.
OrgsWithAccess should be mapped using the references to the entities that is has in the application i.e. assume the class looks something like
public class OrgsWithAccess
{
public virtual Owner { get; set; }
public virtual Broker { get; set; }
public virtual Shipper { get; set; }
}
Then the mapping will look like
public class OrgsWithAccessMap : ClassMap<OrgsWithAccess>
{
public OrgsWithAccessMap()
{
References(x => x.Owner);
References(x => x.Broker);
References(x => x.Shipper);
}
}
Then when querying, you would simply look at the properties on OrgsWithAccess
session.QueryOver<OrgsWithAccess>().Where(x => x.Owner.Id == id);