Linq: Join on multiple kinds of conditions - linq

I'm trying to join on multiple conditions. The problem is that it is a mix of 'equals' and 'not equals'. This previous answer only works if you want to join on 'equals'.
from p1 in context.Set<PersonList>()
join p2 in context.Set<PersonList>()
on p1.Email equals p2.Email && p1.PersonID != p2.PersonID

Just use where to capture the extra conditions:
from p1 in context.Set<PersonList>()
join p2 in context.Set<PersonList>()
on p1.Email equals p2.Email into p2j
from p2 in p2j.DefaultIfEmpty()
where p2 != null && p1.PersonID != p2.PersonID

Related

Multiple join to exclude results in the first join that are in the second one

I use Spring JPA.
I have a transactions entity, Transaction, that need to be paid. Those payouts are recorded in another payout entity, PayoutTransaction. They are linked with a ManyToMany relation. Sometimes payouts fail and they are retried adding a new PayoutTransaction entry, which usually is OK. I need to get the transactions that have a failed payout but excluding those failed transactions that also have a OK payout, as they have been successfully paid even they failed the first attempt (they have another entry in the PayoutTransaction entity with status OK). I have tried it with a multiple JOIN between both tables but it includes the transactions even when they have an OK payout. (Which, I know, is the expected behaviour) How can I solve this?
SELECT distinct t FROM Transaction t JOIN t.payoutTransactions p JOIN t.payoutTransactions p2 where p.status = 'FAILED' and NOT p2.status = 'OK'
You can try EXISTS here,
SELECT distinct t FROM Transaction t JOIN t.payoutTransactions p
where p.status = 'FAILED' AND exists (SELECT p2
payoutTransactions p2 where p2.status = p.status and p2.status <> 'OK')
Or you could even write a native query as below,
SELECT t.* FROM Transaction t join payoutTransaction p on t.ID = p.ID
where p.status = 'FAILED' AND exists
(SELECT 1 FROM payoutTransaction p2 where p2.status = p.status and p2.status <> 'OK' )

SQL to LINQ with JOIN and SubQuery

I have a query that I' struggling to convert to LINQ. I just can't get my head around the required nesting. Here's the query in SQL (just freehand typed):
SELECT V.* FROM V
INNER JOIN VE ON V.ID = VE.V_ID
WHERE VE.USER_ID != #USER_ID
AND V.MAX > (SELECT COUNT(ID) FROM VE
WHERE VE.V_ID = V.ID AND VE.STATUS = 'SELECTED')
The Closest I've come to is this:
var query = from vac in _database.Vacancies
join e in _database.VacancyEngagements
on vac.Id equals e.VacancyId into va
from v in va.DefaultIfEmpty()
where vac.MaxRecruiters > (from ve in _database.VacancyEngagements
where ve.VacancyId == v.Id && ve.Status == Enums.VacanyEngagementStatus.ENGAGED
select ve).Count()
...which correctly resolves the subquery from my SQL statement. But I want to further restrict the returned V rows to only those where the current user does not have a related VE row.
I've realised that the SQL in the question was misleading and whilst it led to technically correct answers, they weren't what I was after. That's my fault for not reviewing the SQL properly so I apologise to #Andy B and #Ivan Stoev for the misleading post. Here's the LINQ that solved the problem for me. As stated in the post I needed to show vacancy rows where no linked vacancyEngagement rows existed. The ! operator provides ability to specify this with a subquery.
var query = from vac in _database.Vacancies
where !_database.VacancyEngagements.Any(ve => (ve.VacancyId == vac.Id && ve.UserId == user.Id))
&& vac.MaxRecruiters > (from ve in _database.VacancyEngagements
where ve.VacancyId == vac.Id && ve.Status == Enums.VacanyEngagementStatus.ENGAGED
select ve).Count()
This should work:
var filterOutUser = <userId you want to filter out>;
var query = from vac in _database.Vacancies
join e in _database.VacancyEngagements
on vac.Id equals e.VacancyId
where (e.UserId != filterOutUser) && vac.MaxRecruiters > (from ve in _database.VacancyEngagements
where ve.VacancyId == vac.Id && ve.Status == Enums.VacanyEngagementStatus.ENGAGED
select ve).Count()
select vac;
I removed the join to VacancyEngagements but if you need columns from that table you can add it back in.

Where Clause on Joined Table with Into Keyword

I wish to join two tables while filtering one of the tables. That works fine like
var matching = from a in ctx.A
join b in ctx.B on a.BId equals b.Id
where idList.Contains(b.Id)
select a;
However, if I also make use of the into keyword to name the joined result
var matching = from a in ctx.A
join b in ctx.B on a.BId equals b.Id into c
where idList.Contains(b.Id)
select a;
I get a compiler error telling me
The name 'b' does not exist in the current context
However, I can reference a at that point, as well as 'c', without problems.
Why is that exactly, and how can I apply a where clause to b?
Why is that exactly
Because after a join into clause, the range variable introduced by that clause isn't in scope - whereas previous variables are. Don't forget that you're joining into c, so each value of b is effectively part of the group of values (c).
and how can I apply a where clause to b?
By doing it earlier:
var matching = from a in ctx.A
join b in ctx.B.Where(x => idList.Contains(x.Id))
on a.BId equals b.Id into c
where c.Any()
select a;
EDIT: This can be put into slightly more query-expression-oriented code as:
var matchingBs = from b in ctx.B
where idList.Contains(b.Id)
select b;
var matching = from a in ctx.A
join b in matchingBs
on a.BId equals b.Id into c
where c.Any()
select a;
(You could use a nested query expression, but I'm not keen on those in general.)
Or using Any on c:
var matching = from a in ctx.A
join b in ctx.B on a.BId equals b.Id into c
where c.Any(b => idList.Contains(b.Id))
select a;
Or even:
var matching = from a in ctx.A
where ctx.B.Any(b => idList.Contains(x.Id) &&
a.BId == b.Id)
select a;
Which can be rewritten as:
var matching = ctx.A.Where(a => ctx.B.Any(b => idList.Contains(x.Id) &&
a.BId == b.Id));
It's important to understand the difference in results between join and join into - the first creates a "pairwise" join; the second creates a group join, where the result for the extra range variable is a group of matches.

LINQ Statement Doesn't Compile

This statement won't compile:
query = from g in context.GridViews
join f in context.GridViewFavorites on g.ID equals f.GridViewID into gf
where g.GridTypeID == id && ( g.IsShared == true || g.RepID == me.clsRep.OID)
&& f.RepID == me.clsRep.OID
select g;
The compiler error is this (and it's underlining the last part of the where clause:
The name 'f' does not exist in the current context
It's logical SQL counterpart would be:
declare #RepID int
declare #GridTypeID int
select #RepID=15, #GridTypeID=5
select g.*,f.*
from
GridViews g
left outer join GridViewFavorites f on f.GridViewID = g.ID
where
g.GridTypeID = #GridTypeID and (g.IsShared = 1 or g.RepID == #RepID)
and f.RepID == #RepID
NOTE: per #hdv 's good catch the SQL sample should actually be:
select g.*,f.*
from
GridView g
left outer join GridViewFavorite f on f.GridViewID = g.ID and f.RepID = #RepID
where
g.GridTypeID = #GridTypeID and (g.IsShared = 1 or g.RepID = #RepID)
It's the "into" part of your join - once you've joined "into" a group, the join variable (f in this case) is out of scope - you've got to use gf instead. Alternatively, given that you're not actually using gf in your query at all, maybe you should just get rid of the into gf part entirely so it's a normal join instead of a group join.
However, that won't give you a left outer join. If you want a left outer join, you might want:
query = from g in context.GridViews
join f in context.GridViewFavorites on g.ID equals f.GridViewID into gf
from f2 in gf.DefaultIfEmpty()
where g.GridTypeID == id && (g.IsShared == true || g.RepID == me.clsRep.OID)
&& (f2 == null || f2.RepID == me.clsRep.OID)
select g;
The left-join pattern for LINQ goes like this:
join f in context.GridViewFavorites on g.ID equals f.GridViewID into gf
from f in gf.DefaultIfEmpty() //missing
More information is available on Stack Overflow and Google.
The where clause acts on the joined result, so the f variable is not in scope.

Linq Acrobatics: How to flatten hierarical data models?

I use SQL like this to flatten hierarchical data. I just create a view and toss it on the EF diagram. However this doesn't fit the "Replace SQL Management Studio with LinqPad" mentality. How would I code these in Linq (and C#)? (Linq to Entities / Entity Framework 4)
Table A holds products and table B holds many kinds of categories. I want to select the category id as a single field in the view:
select A.*, B1.category as color, B2.category as size, B3.category as shape
from A left join B B1 on A.key = B1.key and B1.type = 1 -- Selects one B row
left join B B2 on A.key = B2.key and B2.type = 2
left join B B3 on A.key = B3.key and B3.type = 3
Better yet, is there a Linq pattern cookbook where you can look-up the SQL and see the Linq equivalent? I have already seen the 101 Linq examples in C#.
Unfortunately, there's neither an outer join in LINQ, nor can you add arbitrary join conditions. The inner join can be worked around using DefaultIfEmpty, but the Bn.type = n part of the join condition would need to be moved to a where condition.
The following produces exactly the SQL you provided, except for the type clauses I mentioned:
from A in products
join B1 in categories on A.key equals B1.key into tmp_color
join B2 in categories on A.key equals B2.key into tmp_size
join B3 in categories on A.key equals B3.key into tmp_shape
from B1 in tmp_color.DefaultIfEmpty()
from B2 in tmp_size.DefaultIfEmpty()
from B3 in tmp_shape.DefaultIfEmpty()
where B1.type == 1 && B2.type == 2 && B3.type == 3
select new { product = A, color = B1.category, size = B2.category, shape = B3.category };
results in
exec sp_executesql N'SELECT [t0].[key], [t1].[category] AS [color], [t2].[category] AS [size], [t3].[category] AS [shape]
FROM [Product] AS [t0]
LEFT OUTER JOIN [Category] AS [t1] ON [t0].[key] = [t1].[key]
LEFT OUTER JOIN [Category] AS [t2] ON [t0].[key] = [t2].[key]
LEFT OUTER JOIN [Category] AS [t3] ON [t0].[key] = [t3].[key]
WHERE ([t1].[type] = #p0) AND ([t2].[type] = #p1) AND ([t3].[type] = #p2)',N'#p0 int,#p1 int,#p2 int',#p0=1,#p1=2,#p2=3
(Update: that's LINQ to SQL, just assuming that EF would be similar.)
Albin's answer is more readable, but probably produces less optimal SQL. For an exact match with your SQL, you need to replace FirstOrDefault with DefaultIfEmpty though (might make no difference, depending on your data). (Sorry, can't comment yet ;-))
I would go for a subselect approach.
from a in ModelEntities.A
select new
{
f1 = a.f1,
f2 = a.f2,
// ...,
fn = a.fn,
color = ModelEntities.B.Where(b => a.key == b.key && b.type == 1)
.Select(b => b.category).FirstOrDefault(),
size = ModelEntities.B.Where(b => a.key == b.key && b.type == 2)
.Select(b => b.category).FirstOrDefault(),
shape = ModelEntities.B.Where(b => a.key == b.key && b.type == 3)
.Select(b => b.category).FirstOrDefault(),
}
But following the create a view habit you should probably create some fancy entity in the EF-designer that does something like this.

Resources