Fluent NHibernate: Eager load multiple collections in mapping - c#

Given I have the following entity with multiple collection properties...
public class Parent
{
public virtual int Id { get; set;}
public virtual ICollection<FirstChild> FirstChildren { get; set; }
public virtual ICollection<SecondChild> SecondChildren { get; set; }
}
Is there a way I can simultaneously eager load both of these properties using fluent NHibernate? Or simply eager load everything associated to the Parent.
If I have the following as my mapping...
public ParentMapping()
{
Id(p => p.Id).GeneratedBy.Identity();
HasMany(p => p.FirstChildren)
.Table("FirstChildren")
.KeyColumn("Id")
.Inverse()
.Cascade.AllDeleteOrphan()
.Fetch.Join();
HasMany(p => p.SecondChildren)
.Table("SecondChildren")
.KeyColumn("Id")
.Inverse()
.Cascade.AllDeleteOrphan()
.Fetch.Join();
}
The mapping above results in the error:
'Cannot simultaneously fetch multiple bags'.
Using the Fetch.Join() in the mapping works if I use it on just one of the properties.
I am able to eager load everything by using ToFuture() queries, however, I would prefer to do this in the mapping.

You need to use ISet instead of ICollection to use that feature.
You can take a look here and here.

Related

Strategy to persist entity's associated objects with NHibernate

I am working on a rather larger application that tries to follow a layered architecture pattern. On DBAL I use fluently configured NHibernate. Database objects sometimes have associations like these:
public class HeaderDbo
{
public HeaderDbo()
{
Details = new List<DetailDbo>();
}
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<DetailDbo> Details { get; set; }
}
public class DetailDbo
{
public virtual int Id { get; set; }
public virtual string DetailName { get; set; }
public virtual HeaderDbo Header { get; set; }
public virtual RelevantObjectDbo RelevantObject { get; set; }
}
public class RelevantObjectDbo
{
public virtual int Id { get; set; }
public virtual string RelevantText { get; set; }
}
Mapping is as follows:
public class HeaderDboMap : ClassMap<HeaderDbo>
{
public HeaderDboMap()
{
Table("Header");
Id(x => x.Id).Column("Id");
Map(x => x.Name);
HasMany(x => x.Details)
.Inverse()
.Cascade.All();
}
}
public class DetailDboMap : ClassMap<DetailDbo>
{
public DetailDboMap()
{
Id(x => x.Id).Column("Id");
Table("Detail");
Map(x => x.DetailName);
References(x => x.Header).Column("HeaderId");
References(x => x.RelevantObject).Column("RelevantObjectId")
.Cascade.SaveUpdate(); //??
}
}
public class RelevantObjectDboMap : ClassMap<RelevantObjectDbo>
{
public RelevantObjectDboMap()
{
Id(x => x.Id).Column("Id");
Table("RelevantObject");
Map(x => x.RelevantText);
}
}
Now, there are application domain entities that the DBOs are mapped to which do not necessarily reflect the database structure one-to-one. For example, Header might stay header, but Detail would, say, form itself from DetailDbo and parts of RelevantObjectDbo. Then the application does its thing over the entities - some transformation of Detail happens, which now needs to be persisted.
Suppose I only affected parts of the Detail entity which need to go into Detail table and don't affect RelevantObject table in any way. It might be a wrong way to think about the model but we also need to be practical about how persisting works. So, say, I would like to only have NHibernate update the Detail table without "touching" anything on the RelevantObject table. This is exactly the question, actually: how do I achieve that?
In reality, of course, the DB model is far bigger and more complicated and so is the application logic. There could be a part of BL that does not deal with the RelevantObject part of the data at all, so even though DBOs are loaded from the db fully, not all the data finds its way into the app model. But to persist the data back to the database - it seems that I need to hydrate the DB model fully and it's not always practical. So, how can we instruct NHibernate to "not touch" RelevantObject - in other words, not update dbo.Detail.RelevantObjectId?
I tried applying different Cascade options to DetailDbo.RelevantObject property, but if it stays at null, NHibernate always wants to set RelevantObjectId to NULL - I suppose, rightfully so.
I don't understand how I can write changes to the data that is relevant to my "part" of the BL without having to load and save half of my database through all the associations.
Thank you!
How are you performing these updates?
If you are not modifying anything on RelevantObject NHibernate will not send an update for that table. For example:
var header = session.Get<HeaderDbo>(1);
header.Details.First().DetailName = "Whatever";
session.Flush();
Should not cause an update to be issued to the RelevantObject table.

C# EntityFramework 6.0 - How to use Where Statement in EntityTypeConfiguration?

I have 2 classes like this:
Parent.cs
public class Parent
{
public int Id {get;set;}
public virtual ICollection<Child> Children { get; set; }
}
Child.cs
public class Child
{
public int Id {get;set;}
public ItemStatusType ItemStatusTyp { get; set; }
public int ParentId {get;set;}
[ForeignKey("ParentId")]
public virtual Parent Parent { get; set; }
}
ItemStatusType.cs
public enum ItemStatusType
{
Active = 1,
Deactive = 2,
Deleted = 3
}
What I want is to somehow retrieve always the active ones and not the deleted ones. Since I am not deleting the record physically, I'm merely updating the ItemStatusType to Deleted status.
So, when I say ParentObj.Children I only wish to retrieve the active ones without further using Where condition.
Here is so far what I've done but giving an exception on runtime that I stated afterwards:
public class ParentConfiguration : EntityTypeConfiguration<Parent>
{
public ParentConfiguration()
{
HasMany(c => c.Children.Where(p => p.ItemStatusTyp != ItemStatusType.Deleted).ToList())
.WithRequired(c => c.Parent)
.HasForeignKey(c => c.ParentId)
;
}
}
Runtime Exception:
The expression 'c => c.Children.Where(p => (Convert(p.ItemStatusTyp)
!= 3)).ToList()' is not a valid property expression. The expression
should represent a property: C#: 't => t.MyProperty' VB.Net:
'Function(t) t.MyProperty'.
I had to use ToList after the expression, otherwise it does not compile.
What is the proper what to do what I want?
Thanks in advance,
You cannot use Where or any other logic in fluent property mapping - that's just configuration.
Basically you cannot solve what you need in declarative way.
There are some workarounds which you can use for first-level entities, like implement your own extension(s) MySet<T> which will return .Set<T>.Where(x => x.ItemStatusType != ItemStatusType.Deleted) and use it everywhere, but that won't solve filtering issue for child collections.
You can go hard way and prepare a separate set of entities to use for selecting data, which basically should be based on database views or stored procedures; you will have to create separate view for every entity, so you will be able to combine selecting in any relations based on these views-entities.
For inserting though you will need to have entities mapped over "real" tables. No sure if it worth it but it might in some cases.

Entity Framework Core Two Foreign Keys - Same Table

I'm having problems with having two foreign key references to the same table.
The foreign key id fields are populated but the navigation fields and lists (the Team fields) are not - they are both null.
My classes are:
public class Team
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Fixture> HomeFixtures { get; set; }
public virtual ICollection<Fixture> AwayFixtures { get; set; }
}
public class Fixture
{
public int Id { get; set; }
public int HomeTeamId { get; set; }
public int AwayTeamId { get; set; }
public Team HomeTeam { get; set; }
public Team AwayTeam { get; set; }
}
and my dbContext
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<Team> Teams { get; set; }
public DbSet<Fixture> Fixtures { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Fixture>()
.HasOne(f => f.HomeTeam)
.WithMany(t => t.HomeFixtures)
.HasForeignKey(t => t.HomeTeamId)
.OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior.Restrict);
modelBuilder.Entity<Fixture>()
.HasOne(f => f.AwayTeam)
.WithMany(t => t.AwayFixtures)
.HasForeignKey(t => t.AwayTeamId)
.OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior.Restrict);
}
}
I have tried adding [ForeignKey()] attributes to the HomeTeam and AwayTeam properties but it has no effect.
I have also tried changing the OnModelCreating method to work the other way, i.e.
modelBuilder.Entity<Team>()
.HasMany(t => t.HomeFixtures)
.WithOne(f => f.HomeTeam)
.HasForeignKey(f => f.HomeTeamId)
.OnDelete(Microsoft.EntityFrameworkCore.Metadata.DeleteBehavior.Restrict);
and the same for away fixtures but this produces identical behaviour.
It doesn't seem to matter how I query but the simplest case is
Fixture fixture = await _context.Fixtures.SingleOrDefaultAsync(f => f.Id == id);
The returned fixture object contains team Ids that are valid and in the database but the Team objects are still not populated.
Has anyone any idea what I'm doing wrong?
This is a brand new project and a brand new database so there's no legacy code interfering.
I'm using Visual Studio 2017rc with Entity Framework Core.
At present EF Core does not support lazy loading. Tracking issue here
That means by default navigation properties will not be loaded and will remain null. As a work-around you can use eager loading or explicit loading patterns.
Eager Loading
Eager loading is pattern where you request for the referenced data you need eagerly while running the query using Include API. The usage is somewhat different from how it worked in EF6. To include any navigation, you specify the lambda expression (or string name) in include method in your query.
e.g. await _context.Fixtures.Include(f => f.HomeTeam).FirstOrDefaultAsync(f => f.Id == id);
Presently, filtered include is not supported so you can request to load navigation fully or exclude it. So the lambda expression cannot be complex. It must be simple property access. Also to load nested navigation, you can either chain them with property access calls (like a.b.c) or when its after collection navigation (since you cannot chain them) use ThenInclude.
e.g. await _context.Fixtures.Include(f => f.HomeTeam).ThenInclude(t=> t.HomeFixtures).FirstOrDefaultAsync(f => f.Id == id);
It would be good to remember that include represent a path of navigation from the entity type its being called on to populate all naviagations on the path. Often you may need to write repeated calls if you are including multiple navigations at 2nd or further level. That is just for syntax though and query will be optimized and will not do repeated work. Also with string include you can specify whole navigation path without needing to use ThenInclude. Since reference navigation can utilize join to fetch all data needed in single query, & collection navigation can load all related data in single query, eager loading is most performant way.
Explicit Loading
When you have loaded object in the memory and need to load a navigation, while lazy loading would have loaded it while accessing navigation property, in the absence of that you need to call Load method by yourself. These methods are defined on ReferenceEntry or CollectionEntry.
e.g.
Fixture fixture = await _context.Fixtures.SingleOrDefaultAsync(f => f.Id == id);
_context.Entry(fixture).Reference(f => f.HomeTeam).Load();
var team = await _context.Teams.SingleOrDefaultAsync(t => t.Id == id);
_context.Entry(team).Collection(f => f.HomeFixtures).Load();
For reference navigation you would need Reference on EntityEntry to get ReferenceEntry. For collection navigation equivalent method is Collection. Then you just call Load method on it to load the data in the navigation. There is also async version of LoadAsync if you need.
Hope this helps.

Entity Framework does not eager load when using Include()

I have a simple model and a simple query. I'm trying to get EF to eager load my navigation properties:
// Document object has a navigation property "DocType" of type DocType
// DocType object has a navigation property "Documents" of type ICollection<Documents> that is NOT virutal
context.Set<Document>().Where(d=>d.Id == id).Include(d=>d.DocType).SingleOrDefault();
The problem is that this doesn't actually eager load DocType. The stranger thing is that excluding the Include() call does load the DocType property, but as a second query.
I've looked all around and applied every fix I found:
Added a call to Include()
Removed virtual from both navigation properties
Any idea what's going on here? Is it possible to coerce EF to merge this into a single query that's eager loaded?
EDIT: This is my data model:
namespace Data.Models {
class Document {
public int Id { get; set;}
public int TypeId { get; set; }
public DocType DocType { get; set; }
}
class DocType {
public int Id { get; set; }
public string FullName { get; set; }
public ICollection<Document> Documents { get; set; }
}
}
namespace Data.Mappings {
internal class DocumentTypeConfiguration : EntityTypeConfiguration<Document> {
public DocumentTypeConfiguration() {
ToTable("ProsDocs");
HasKey(m => m.Id);
Property(m => m.Id)
.HasColumnName("ProsDocId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(m => m.TypeId)
.HasColumnName("ProsDocTypeId")
.HasMaxLength(3);
HasRequired(d => d.DocType)
.WithMany(dt=>dt.Documents)
.WithForeignKey(d=>d.TypeId);
}
}
internal class DocTypeTypeConfiguration : EntityTypeConfiguration<DocType> {
public DocTypeTypeConfiguration() {
ToTable("DocType");
HasKey(m => m.Id);
Property(m => m.Id)
.HasColumnName("DocTypeId")
.HasMaxLength(4);
Property(m => m.FullName)
.HasColumnName("DocTypeDesc")
.HasMaxLength(255);
}
}
}
The oddest thing is that when I call:
context.Set<Document>().Find(id);
The DocType properties are populated, but EF does this by executing two separate queries. Is it possible to design this in such a way the EF understands that this can be accomplished with one query?
EDIT 2: This question seems to address the same problem, but only states the calling Include() fixes it, which is not working in my case.
To include navigation properties I use this syntax (with quotes in the Include):
context.Documents.Where(d=>d.Id == id).Include("DocType").SingleOrDefault();
In general case, using string instead of expression (as mention #vanraidex) is not a good practice. In general case. However, when using third party providers (e.g. Oracle Provider) it can be the only way to get correct sql (with joins).
So, if you using special Data Provider and .Include() method doesn't work, try to use string instead of expression.
context.Documents.Where(d=>d.Id == id).Include("DocType").SingleOrDefault();

Fluent NHibernate HasMany Collection Problems

Update:
It appears that changing my mapping from Cascade.All() to Cascade.AllDeleteOrphan() fixes most of my issues. I still have to explicitly set the Company property on the OperatingState, which seems unnecessary as it's being added to the Company entity, but at least I can work with that during an update. I still need to test that with a create.
If any one can explain that, that would be a big help.
Update 2: After playing with it some more, it appears I don't always have to specify the parent entity.
Original Post
I have 2 related entities
public class Company {
//... fields
public virtual IList<OperatingState> OperatingStates { get; set; }
}
public class OperatingState {
public virtual Company Company { get; set; }// Mapped on CompanyID
public virtual string State { get; set; }
}
And they are mapped like this:
public class CompanyMap : ClassMap<Company> {
public CompanyMap() {
//... fields
HasMany(x => x.OperatingStates)
.Cascade.All()
.Table("OperatingState");
}
}
public class OperatingStateMap : ClassMap<OperatingState> {
public OperatingStateStateMap() {
Id(x => x.ID);
References(x => x.Company);
Map(x => x.State);
}
}
So all is well until I try to update Company with new Operating States
Company company = _repo.GetSingle(123);
company.OperatingStates.Clear();
foreach(string state in form["OperatingStates"].Split(',')) {
company.OperatingStates.Add(new OperatingState(state));
}
_repo.Save(company); // calls ISession.SaveOrUpdate
It bombs out with:
Cannot insert the value NULL into
column 'CompanyID', table
'ConsumerCartel.dbo.CompanyOperatingState';
column does not allow nulls. INSERT
fails. The statement has been
terminated.
However, if I make 2 changes it kind of works
Company company = _repo.GetSingle(123);
// don't clear the list this time;
foreach(string state in form["OperatingStates"].Split(',')) {
OperatingState os = new OperatingState(state);
// explicitly setting the company
os.Company = company;
company.OperatingStates.Add(os);
}
_repo.Save(company); // calls ISession.SaveOrUpdate
Which will add the new states in addition to the old ones, which is not what I want. However, even when explicitly setting the company (which I shouldn't have to do when it's added to a mapped list?) it doesn't work if the list is cleared.
This code has worked on other entities, but not on this one, so I think this should work as written. What am I doing wrong?
Have you tried using Inverse()?
HasMany(x => x.OperatingStates)
.Inverse()
.Cascade.All()
.Table("OperatingState");

Categories