LINQ: Self join query, how to accomplish this? - linq

Can anyone help?
I have 1 class, basically it holds Members and within that class is a List.
The members i have in a List also... So basically it goes like this,
I have 2 members and each member has a number of sessions.
I wish to only return each member with 1 Session.
I have done a LINQ query, but of course it doesn't work...
I think i need to do a self join, any ideas?
Basically my error is m doesn't exist in my subquery self join.
var sessions =
from m in this.members
join s in
(
from se in m.Sessions
group se by se.Name into g
select new {Name = g.Key, SessioEndTime = g.Max(a=>a.SessioEndTime)}
)
on m.Name equals s.Name
select new { MemberName = m.Name, SessionTime = s.SessioEndTime}
I would appreciate any feedback anyone has.
Thanks in advance.
EDIT
Ok i managed to do it like the following, but is this the best way?
var sessions =
from m in this.members
let sn = m.Sessions.OrderByDescending(a => a.SessionEndTime).FirstOrDefault()
select new { MemberName = m.Name, SessionTime = sn.SessioEndTime}
This way sn contains 1 record, but i have access to all the properties...
But is this the best way to do using a LET?
Thanks.

Unless I am missing something you need this, no?
var sessions =
from m in members
select new {
MemberName = m.Name,
SessionTime = m.Sessions.Max(s => s.SessioEndTime)
};
You have to change the way you think about LINQ queries, think more from object point rather than from SQL implementation point. What is it that I need? I need all members, each with its latest session end time, then act on that.
EDIT:
The let option you used is ok, just keep something in mind FirstOrDefault will return null if member has an empty list of Sessions, and then sn.SessionEndTime hits null reference. If on the other hand you are certain that every member has at least one session use First instead or aggregate.
Also don't use FirstOrDefault() in the let, it kind of messes up the LINQ and prevents it from tying it to the master (causing a separate SQL query for each master to detect missing subsets), so usable queries with let are:
from m in Members
let sn = m.Sessions.Max(s => s.SessioEndTime)
select new { MemberName = m.Name, SessionTime = sn};
from m in Members
let sn = m.Sessions.OrderByDescending(a => a.SessioEndTime).First()
select new { MemberName = m.Name, SessionTime = sn.SessioEndTime};
As for ordering vs Max aggregation, both queries will generate a subquery:
-- MAX
SELECT [t0].[Name] AS [MemberName], (
SELECT MAX([t1].[SessioEndTime])
FROM [Session] AS [t1]
WHERE [t1].[memberId] = [t0].[id]
) AS [SessionTime]
FROM [Member] AS [t0]
GO
-- ordering
SELECT [t0].[Name] AS [MemberName], (
SELECT [t2].[SessioEndTime]
FROM (
SELECT TOP (1) [t1].[SessioEndTime]
FROM [Session] AS [t1]
WHERE [t1].[memberId] = [t0].[id]
ORDER BY [t1].[SessioEndTime] DESC
) AS [t2]
) AS [SessionTime]
FROM [Member] AS [t0]
With a descending index on SessioEndTime the ordering script is about twice slower (you can get execution plans for these to check for yourself), without the index its about 5times slower.

Related

EF Linq query with conditional include

So I have the following Linq query:
var member = (from mem in
context.Members.Include(m =>
m.MemberProjects.Select(mp => mp.Project))
where mem.MemberId == memberId
select mem).FirstOrDefault();
This returns a Member entity, with a set of MemberProjects that have a Project child. I would like to limit the MemberProjects to only those for which the Project child has a property
ProjectIdParent == null.
One of my failed attempts might make the intent clearer:
var member = (from mem in context.Members
.Include(m => m.MemberProjects
.Where(mp =>
mp.Project.ProjectIdParent == null)
.Select(proj => proj.Project))
where mem.MemberId == memberId
select mem).FirstOrDefault();
This of course complains of an invalid Include expression because of the Where clause.
Any thoughts on how to do this would be great :)
DISCLAIMER: I havent tested this. This is just an idea. If you let me know the results, I will update this accordingly. (Skip to the update part for the tested solutions)
var member = (from mps in context.MemberProjects
.Include(m => m.Members)
.Include(m => m.Projects)
where mps.Project.ProjectIdParent == null
select mps)
.FirstOrDefault(mprojs => mprojs.Member.MemberId == memberId);
I'd also analyze the queries using something like EFProfiler to make sure the generated queries dont leave the realm of sanity.
You can also take a look at this post by Jimmy Bogard on Many to Many relationships with ORMs.
Update
I came up with multiple tested solutions for this with EF 6.1.3. My Edmx looked like below:
The setup data is like below:
I was able to run code below to get the MemberFive correctly
var member = context.Members.FirstOrDefault
(m => m.MemberId == memberId
&& m.Projects.Any(p => p.ProjectParentId == null));
The generated SQL looked like this:
SELECT TOP (1) [Extent1].[MemberId] AS [MemberId],
[Extent1].[MemberName] AS [MemberName]
FROM [dbo].[Members] AS [Extent1]
WHERE ([Extent1].[MemberId] = 1)
AND (EXISTS (SELECT 1 AS [C1]
FROM (SELECT [MemberProjects].[MemberId] AS [MemberId],
[MemberProjects].[ProjectId] AS [ProjectId]
FROM [dbo].[MemberProjects] AS [MemberProjects])
AS [Extent2]
INNER JOIN [dbo].[Projects] AS [Extent3]
ON [Extent3].[ProjectId] = [Extent2].[ProjectId]
WHERE ([Extent1].[MemberId] = [Extent2].[MemberId])
AND ([Extent3].[ProjectParentId] IS NULL)))
If you dont like the generated query you can use this:
var memberQuery = #"Select M.* from Members M
inner join MemberProjects MP on M.MemberId = Mp.ProjectId
inner join Projects P on MP.ProjectId = P.ProjectId
where M.MemberId = #MemberId and P.ProjectParentId is NULL";
var memberParams = new[]
{
new SqlParameter("#MemberId", 1)
};
var member3 = context.Members.SqlQuery(memberQuery, memberParams)
.FirstOrDefault();
The later consistently returned under 20ms vs the other one hovered around 60ms (if that matters to you).
I hope this helps.

LINQ to Entities three table join query

I'm having a bit trouble with a query in Linq to Entities which I hope someone can shed a light on :-) What I'm trying to do is to create a query that joins three tables.
So far it works, but since the last table I'm trying to join is empty, the result of the query doesn't contain any records. When I remove the last join, it gives me the right results.
My query looks like this:
var query = from p in db.QuizParticipants
join points in db.ParticipantPoints on p.id
equals points.participantId into participantsGroup
from po in participantsGroup
join winners in db.Winners on p.id
equals winners.participantId into winnersGroup
from w in winnersGroup
where p.hasAttended == 1 && p.weeknumber == weeknumber
select new
{
ParticipantId = p.id,
HasAttended = p.hasAttended,
Weeknumber = p.weeknumber,
UmbracoMemberId = p.umbMemberId,
Points = po.points,
HasWonFirstPrize = w.hasWonFirstPrize,
HasWonVoucher = w.hasWonVoucher
};
What I would like is to get some records even if the Winners table is empty or there is no match in it.
Any help/hint on this is greatly appreciated! :-)
Thanks a lot in advance.
/ Bo
If you set these up as related entities instead of doing joins, I think it will be easier to do what you're trying to do.
var query = from p in db.QuizParticipants
where p.hasAttended == 1 && p.weeknumber == weeknumber
select new
{
ParticipantId = p.id,
HasAttended = p.hasAttended,
Weeknumber = p.weeknumber,
UmbracoMemberId = p.umbMemberId,
Points = p.ParticipantPoints.Sum(pts => pts.points),
HasWonFirstPrize = p.Winners.Any(w => w.hasWonFirstPrize),
HasWonVoucher = p.Winners.Any(w => w.hasWonVoucher)
};
This is assuming hasWonFirstPrize and hasWonVoucher are boolean fields, but you can use any aggregate function to get the results you need, such as p.Winners.Any(w => w.hasWonFirstPrize == 1)
I don't use query syntax a lot but I believe you need to change from w in winnersGroup to from w in winnersGroup.DefaultIfEmpty()

ef and linq extension method

I have this sql that i want to have written in linq extension method returning an entity from my edm:
SELECT p.[Id],p.[Firstname],p.[Lastname],prt.[AddressId],prt.[Street],prt.[City]
FROM [Person] p
CROSS APPLY (
SELECT TOP(1) pa.[AddressId],a.[ValidFrom],a.[Street],a.[City]
FROM [Person_Addresses] pa
LEFT OUTER JOIN [Addresses] AS a
ON a.[Id] = pa.[AddressId]
WHERE p.[Id] = pa.[PersonId]
ORDER BY a.[ValidFrom] DESC ) prt
Also could this be re-written in linq extension method using 3 joins?
Assuming you have set the Person_Addresses table up as a pure relation table (i.e., with no data besides the foreign keys) this should do the trick:
var persons = model.People
.Select(p => new { p = p, a = p.Addresses.OrderByDescending(a=>a.ValidFrom).First() })
.Select(p => new { p.p.Id, p.p.Firstname, p.p.LastName, AddressId = p.a.Id, p.a.Street, p.a.City });
The first Select() orders the addresses and picks the latest one, and the second one returns an anonymous type with the properties specified in your query.
If you have more data in your relation table you're gonna have to use joins but this way you're free from them. In my opinion, this is more easy to read.
NOTE: You might get an exception if any entry in Persons have no addresses connected to them, although I haven't tried it out.

Linq Query with aggregate function

I am trying to figure out how to go about writing a linq query to perform an aggregate like the sql query below:
select d.ID, d.FIRST_NAME, d.LAST_NAME, count(s.id) as design_count
from tbldesigner d inner join
TBLDESIGN s on d.ID = s.DESIGNER_ID
where s.COMPLETED = 1 and d.ACTIVE = 1
group by d.ID, d.FIRST_NAME, d.LAST_NAME
Having COUNT(s.id) > 0
If this is even possible with a linq query could somebody please provide me with an example.
Thanks in Advance,
Billy
A more direct translation of your original SQL query would look like this:
var q =
// Join tables TblDesign with TblDesigner and filter them
from d in db.TblDesigner
join s in db.TblDesign on d.ID equals s.DesignerID
where s.Completed && d.Active
// Key and values used for grouping (note, you don't really need the
// value here, because you only need Count of the values in a group, but
// in case you needed anything from 's' or 'd' in 'select', you'd write this
let value = new { s, d }
let key = new { d.ID, d.FirstName, d.LastName }
group value by key into g
// Now, filter the created groups (return only non-empty) and select
// information for every group
where g.Count() > 0
select { ID = g.Key.ID, FirstName = g.Key.FirstName,
LastName = g.Key.LastName, Count = g.Count() };
The HAVING clause is translated to an ordinary where that is applied after grouping values using group ... by. The result of grouping is a collection of groups (another collections), so you can use where to filter groups. In the select clause, you can then return information from the key (used for grouping) and aggregate of values (using g.Count())
EDIT: As mmcteam points out (see comments), the where g.Count() > 0 clause is not necessary, because this is already guranteed by the join. I'll leave it there, because it shows how to translate HAVING clause in general, so it may be helpful in other cases.
Here's how I'd do it. Please note that I'm accustomed to linqtosql and am unaware if there are differences for the query in linqtoentities.
var query =
from d in myObjectContext.tbldesigner
where d.ACTIVE == 1
let manys =
from s in d.tbldesign
where s.COMPLETED == 1
select s
where manys.Count() > 0
select new
{
d.ID, d.FIRST_NAME, d.LAST_NAME,
DesignCount = manys.Count()
};
Ignoring the s.id which is confusing me (see my comment on the question), this is a simple query which would generate a having clause. Of course, in this case it's a worthless example since the count will always be more than 0 in this case.
Anyways, if you are using SQL to Entities, you should use the entity mapping to access the foreign key relationships instead of manually doing a join or a subquery.
var results = from d in db.tbldesigner
where d.TBLDESIGN.COMPLETED && d.ACTIVE
group d by new {d.ID, d.FIRST_NAME, d.LAST_NAME} into g
where g.Count() >= 0
select new {
d.ID, d.FIRST_NAME, d.LAST_NAME,
Count = g.Count()
};
NOTE: This is untested (and uncompiled) so there might be some issues, but this is where I would start.

Stuck on a subquery that is grouping, in Linq`

I have some Linq code and it's working fine. It's a query that has a subquery in the Where clause. This subquery is doing a groupby. Works great.
The problem is that I don't know how to grab one of the results from the subquery out of the subquery into the parent.
Frst, here's the code. After that, I'll expplain what piece of data i'm wanting to extract.
var results = (from a in db.tblProducts
where (from r in db.tblReviews
where r.IdUserModified == 1
group r by
new
{
r.tblAddress.IdProductCode_Alpha,
r.tblAddress.IdProductCode_Beta,
r.tblAddress.IdProductCode_Gamma
}
into productGroup
orderby productGroup.Count() descending
select
new
{
productGroup.Key.IdProductCode_Alpha,
productGroup.Key.IdProductCode_Beta,
productGroup.Key.IdProductCode_Gamma,
ReviewCount = productGroup.Count()
}).Take(3)
.Any(
r =>
r.IdProductCode_Alpha== a.IdProductCode_Alpha&&
r.IdProductCode_Beta== a.IdProductCode_Beta&&
r.IdProductCode_Gamma== a.IdProductCode_Gamma)
where a.ProductFirstName == ""
select new {a.IdProduct, a.FullName}).ToList();
Ok. I've changed some field and tables names to protect the innocent. :)
See this last line :-
select new {a.IdProduct, a.FullName}).ToList();
I wish to include in that the ReviewCount (from the subquery). I'm jus not sure how.
To help understand the problem, this is what the data looks like.
Sub Query
IdProductCode_Alpha = 1, IdProductCode_Beta = 2, IdProductCode_Gamma = 3, ReviewCount = 10
... row 2 ...
... row 3 ...
Parent Query
IdProduct = 69, FullName = 'Jon Skeet's Wonder Balm'
So the subquery grabs the actual data i need. The parent query determines the correct product, based on the subquery filters.
EDIT 1: Schema
tblProducts
IdProductCode
FullName
ProductFirstName
tblReviews (each product has zero to many reviews)
IdProduct
IdProductCode_Alpha (can be null)
IdProductCode_Beta (can be null)
IdProductCode_Gamma (can be null)
IdPerson
So i'm trying to find the top 3 products a person has done reviews on.
The linq works perfectly... except i just don't know how to include the COUNT in the parent query (ie. pull that result from the subquery).
Cheers :)
Got it myself. Take note of the double from at the start of the query, then the Any() being replaced by a Where() clause.
var results = (from a in db.tblProducts
from g in (
from r in db.tblReviews
where r.IdUserModified == 1
group r by
new
{
r.tblAddress.IdProductCode_Alpha,
r.tblAddress.IdProductCode_Beta,
r.tblAddress.IdProductCode_Gamma
}
into productGroup
orderby productGroup.Count() descending
select
new
{
productGroup.Key.IdProductCode_Alpha,
productGroup.Key.IdProductCode_Beta,
productGroup.Key.IdProductCode_Gamma,
ReviewCount = productGroup.Count()
})
.Take(3)
Where(g.IdProductCode_Alpha== a.IdProductCode_Alpha&&
g.IdProductCode_Beta== a.IdProductCode_Beta&&
g.IdProductCode_Gamma== a.IdProductCode_Gamma)
where a.ProductFirstName == ""
select new {a.IdProduct, a.FullName, g.ReviewCount}).ToList();
While I don't understand LINQ completely, but wouldn't the JOIN work?
I know my answer doesn't help but it looks like you need a JOIN with the inner table(?).
I agree with shahkalpesh, both about the schema and the join.
You should be able to refactor...
r => r.IdProductCode_Alpha == a.IdProductCode_Alpha &&
r.IdProductCode_Beta == a.IdProductCode_Beta &&
r.IdProductCode_Gamma == a.IdProductCode_Gamma
into an inner join with tblProducts.

Resources