Health record may have Symptom, which consists of some Words. (ER diagram.)
What I need: by given set of Words return Health records with corresponding Symptoms.
I have this code:
public IEnumerable<HealthRecord> GetByWords(IEnumerable<Word> words)
{
var wordsIds = words.Select(w => w.Id).ToList();
Word word = null;
HealthRecord hr = null;
ISession session = NHibernateHelper.GetSession();
{
return session.QueryOver<HealthRecord>(() => hr)
.WhereRestrictionOn(() => hr.Symptom).IsNotNull()
.Inner.JoinAlias(() => hr.Symptom.Words, () => word)
.WhereRestrictionOn(() => word.Id).IsIn(wordsIds)
.List();
}
}
What we should use here is: INNER SELECT, i.e. subquery. We can do that even with many-to-many maping, but the performance will suffer.
The (easier, my prefered) way would be to not use many-to-many mapping. Because with explicitly mapped pairing object SymptomWord, querying would be much more easier.
Word word = null;
Symptom symptom = null;
// the sub SELECT returning column Symptom.Id
var subq = QueryOver.Of<Symptom>(() => symptom)
// just symptoms refering the searched words
.Inner.JoinAlias(() => symptom.Words, () => word)
.WhereRestrictionOn(() => word.Id).IsIn(wordsIds)
// the result of inner select is
.Select(s => symptom.Id);
And in the next step we can use it for filtering:
var list = session
// just query over HealthRecord here
.QueryOver<HealthRecord>()
.WithSubquery
// the ID of referenced Symptom is in that table
.WhereProperty(hr => hr.Symptom.Id)
// and will be filtered with our subquery
.In(subq)
.List<HelthRecord>();
return list;
That should work, also check some similar issue here:
Query on HasMany reference
NHibernate Lazy Loading Limited by Earlier Criteria
Some hint how to re-map many-to-many (because with a pairing table mapped as an object, we can construct similar and simplified construct, resulting in better SQL Statement)
Nhibernate: How to represent Many-To-Many relationships with One-to-Many relationships?
Related
I have seen many questions related to how you can return a limited set of fields from a single EF entity with anonymous types. My issue is about the opposite of that. I want to return values from related tables along with all of the fields in my entity table:
IQueryable<EntityModels.TBLEFFORT> query = db.TBLEFFORT.AsQueryable();
query = query.Where(a => a.EFFDELETE == "0");
if (!string.IsNullOrEmpty(sGuid))
{
query = query.Where(a => a.TBLEFFORTLINK.Any(b => b.TBLSHS.TBLTACT.Any(c => c.CGUID == sGuid)));
}
query.Select(x => new EffortSearchResult()
{
Efguid = x.EFGUID,
Efstatus = x.EFSTATUS
});
Here my base entity is TBLEFFORT and i query directly against it on the EFFDELETE field and then i query against it with a related table TBLSHS.
However, in my anon return object i only have fields from TBLEFFORT (EFGUID and EFSTATUS). How can i include fields from the related TBLSHS entity in my anonymous return object?
The related tables are lookups so they will be 1:1. I'm not looking to return complex sets based on a FK field in TBLEFFORT
I figured it out. you just extend out in your anon type and dig for what you need from the related tables:
Shguid = x.TBLEFFORTLINK.Select(b => b.TBLSHS.SOMEFIELD).FirstOrDefault(),
I have Entity 'Content'. Each Content has a 'Placement' property. Placement has a many-to-many relationship width 'AdType' entity (Placement has IList<\AdType> property mapped).
I need to load all Placements that are used at least in one Content and associated width specified AdType.
My DAL function looks like this:
public IList<Placement> Load(AdType adType)
{
return NHibernateSession.QueryOver<Content>()
.JoinQueryOver(content => content.Placement)
.JoinQueryOver<AdType>(placement => placement.AdTypes)
.Where(_adType => _adType.Id == adType.Id)
.Select(x => x.Placement).List<Placement>();
}
This works fine but when I look to the SQL log i see:
SELECT this_.PlacementId as y0_ FROM AdManager.dbo.[Content] this_ inner join AdManager.dbo.[Placement] placement1_ on this_.PlacementId=placement1_.PlacementId inner join AdManager.dbo.AdTypeToPlacement adtypes5_ on placement1_.PlacementId=adtypes5_.PlacementId inner join AdManager.dbo.[AdType] adtype2_ on adtypes5_.AdTypeId=adtype2_.AdTypeId WHERE adtype2_.AdTypeId = #p0
SELECT placement0_.PlacementId as Placemen1_26_0_, placement0_.Name as Name26_0_ FROM AdManager.dbo.[Placement] placement0_ WHERE placement0_.PlacementId=#p0
SELECT placement0_.PlacementId as Placemen1_26_0_, placement0_.Name as Name26_0_ FROM AdManager.dbo.[Placement] placement0_ WHERE placement0_.PlacementId=#p0
This means that NHibernate takes all placements Id in first query and then queries all fields from Placement table by Id.
My question is: Does enyone know how to modify QueryOver method to force NHibernate load data in one query?
it seems NHibernate does think there might be something in the where which maybe filters out data which is needed tro initialize the placement. You can go with a subquery:
public IList<Placement> Load(AdType adType)
{
var subquery = QueryOver.For<Content>()
.JoinQueryOver(content => content.Placement)
.JoinQueryOver<AdType>(placement => placement.AdTypes)
.Where(_adType => _adType.Id == adType.Id)
.Select(x => x.Id);
return NHibernateSession.QueryOver<Content>()
.WithSubquery.Where(content => content.Id).IsIn(subquery))
//.Fetch(x => x.Placement).Eager try with and without
.Select(x => x.Placement).List<Placement>();
}
or SQL (has the disadvantage that it just fills the new Placement but doest track it)
public IList<Placement> Load(AdType adType)
{
return NHibernateSession.CreateSQLQuery("SELECT p.Name as Name, ... FROM content c join placement p...")
.SetResultTransformer(Transformers.AliastoBean<Placement>())
.List<Placement>();
}
I have a parent-child relationship between Teacher and StudentReport. Each StudentReport has a timestamp field recording when the teacher finished the report. I have a query to find all teachers who have completed one or more of their reports as of a certain number of minutes ago:
public IList<Teacher> FindRecentlyActiveTeachers(int intervalMinutes)
{
if (intervalMinutes <= 0)
throw new ArgumentException("Interval must be a positive number of minutes");
DateTime activityCutoff = DateTime.Now.AddMinutes(-1 * intervalMinutes);
return Session.QueryOver<Teacher>()
.Left.JoinQueryOver<StudentReport>(t => t.StudentReports)
.Where(r => r.FirstSaveTimestamp >= activityCutoff)
.TransformUsing(Transformers.DistinctRootEntity)
.List<Teacher>();
}
This returns the correct list of teachers, but the child collection for each teacher only contains the reports that match the selection criterion. I would like the report collection of each matching teacher to contain all of the reports, not just the few reports that meet the criteria.
Is there some way I can either load the full child collection eagerly, or omit loading it in this query and rely on lazy loading it?
Update
This is the solution:
return Session.QueryOver<Teacher>()
.Fetch(t => t.StudentReports).Eager
.JoinQueryOver<StudentReport>(t => t.StudentReports)
.Where(r => r.FirstSaveTimestamp >= activityCutoff)
.TransformUsing(Transformers.DistinctRootEntity)
.List<Teacher>();
Use Fetch
return Session.QueryOver<Teacher>()
.Fetch(t => t.StudentReports)
.Where(r => r.FirstSaveTimestamp >= activityCutoff)
.TransformUsing(Transformers.DistinctRootEntity)
.List<Teacher>();
Is it possible to use a multiquery and have two hql queries returning two different sets of entities where one of the sets are used in the other and that the session "fixes" this via the first level cache?
E.g. scenario (a dumb one and it could be solved with joins)
public class Room
{
...
public virtual ISet<Bookings> Bookings {get;set;}
public virtual bool IsAvailible {get;set;}
...
}
public class Booking
{
...
}
After executing a multicriteria with two hql's:
returning all rooms where
IsAvailible = true
returning all bookings having a room that has a room that IsAvailible
when accessing a room from the result and its bookings I want them to be resolved from the second resultset via the firstlevel cache of the session and there by avoiding n+1.
Generally speaking, NHibernate can use the cache to "combine" the results from queries executed through Multiquery. However, it should be noted that this usually only applies to cases where lazy collections are loaded with no restrictions whatsoever.
Examples:
Invoice iAlias = null;
InvoiceDetails idAlias = null;
// Base-Query: get Invoices with certain condition
var invoices = session.QueryOver<Invoice>()
.Where(i => i.Number == "001")
.Future<Invoice>();
// Option 1: this will still cause N+1 if we iterate through invoices,
// because it doesn't know better
var invoicedetails = session.QueryOver<InvoiceDetails>()
.JoinAlias(a => a.Invoice, () => iAlias)
.Where(() => iAlias.Number == "001")
.Future<InvoiceDetails>();
// Option 2: this will still cause N+1 if we iterate through invoices,
// because we limited the possible results using a where-condition
var invoices2 = session.QueryOver<Invoice>()
.Left.JoinAlias(i => i.Details, () => idAlias)
.Where(i => i.Number == "001")
.And(() => idAlias.Quantity > 5)
.Future<Invoice>();
// Option 3: this will work without N+1, because we don't use a filter
// -> NHibernate will use the collection in cache
var invoices3 = session.QueryOver<Invoice>()
.Left.JoinAlias(i => i.Details, () => idAlias)
.Where(i => i.Number == "001")
.Future<Invoice>();
foreach (Invoice i in invoices)
{
int count = i.Details.Count;
}
If we comment out two of the three options and execute the code, we will see that only option 3 will prevent a N+1, the other two will still load the InvoiceDetails for each Invoice in the loop.
Of course this is a very simple example and it is obvious that Option 3 could also be executed without the Base-query and still return the same result, but I hope you get the idea.
In the case where we load two different sets of entities, i.e. the root class is different as in Option 1, this "combining" will most likely not work.
Sorry, if I used QueryOver instead of HQL, but the same rules apply.
Gyus, keep in mind that sometimes you can have similar problems because of
LeftOuterJoin is not set.
.JoinAlias(x => x.Prop, () => propAlias, JoinType.LeftOuterJoin)
Querying child collections has been a recurring issue in our applications where we use NHibernate (via LINQ). I want to figure out how to do it right. I just tried forever to get this query to work efficiently using LINQ, and gave up. Can someone help me understand the best way to do something like this?
Model: ServiceProvider
HasMany->ServicesProvided
The gotcha here is that the HasMany is mapped as a component, so I can't directly query the ServicesProvided. For posterity's sake, here's the mapping:
public ServiceProviderMap()
{
DiscriminatorValue(ProfileType.SERVICE_PROVIDER.ID);
HasMany(p => p.ServicesProvided)
.Table("ServiceProvider_ServicesProvided")
.KeyColumn("ProfileID")
.Component(spMapping =>
{
spMapping.Map(service => service.ID)
.Not.Nullable();
})
.AsBag();
}
The query I am trying to create would return a collection of the count of each service that is provided. IE: Service1 -> 200, Service2 -> 465, etc.
I was able to get the query working using HQL, so here it is. Note that it just returns the ID of the service that is provided:
select service.ID, count(service)
from ServiceProvider as profile
inner join profile.ServicesProvided as service
group by service.ID
I was able to get the query "working" using LINQ, but it performed atrociously. Here's the code I used (warning - it's ugly).
Func<ServiceProvider, IEnumerable<ServicesProvided>> childSelector = sp => sp.ServicesProvided;
var counts = this._sessionManager.GetCurrentSession().Linq<ServiceProvider>()
.Expand("ServicesProvided")
.SelectMany(childSelector, (t, c) => new { t = t, c = c })
.Select(child => child.c)
.GroupBy(sp => sp.ID)
.Select(el => new { serviceID = el.Key, count = el.Count() });
I would love to learn how to do this correctly, please.
Short of going with HQL, the most elegant solution I can think of would be using a Criteria object. The following will give you what you need and with very low overhead:
ICriteria criteria = this._sessionManager.GetCurrentSession().CreateCriteria(typeof(ServiceProvider), "sp");
//set projections for the field and aggregate, making sure to group by the appropriate value
criteria.CreateAlias("sp.ServicesProvided", "s", JoinType.LeftOuterJoin)
.SetProjection(Projections.ProjectionList()
.Add(Projections.Property("s.ID"), "serviceID")
.Add(Projections.Count("sp.ID"), "count")
.Add(Projections.GroupProperty("s.ID")));
IList<object[]> results = criteria.List();
foreach (object[] entry in results)
{
int id = (int)entry[0], qty = (int)entry[1];
//Do stuff with the values
}