Why EF 6.13 left outer join with Lambda DefaultIfEmpty is Invalid? - entity-framework

Sorry, my English is very poor, I hope you understand.
If I use Linq it's correct, but now I want to use Lambda.
TABLE 1
ExamQuestion
TABLE 2
PaperQuestion
No foreign key;
var l = db.ExamQuestions
.Join(db.PaperQuestions, s => s.Id, p => p.QuestionId, (s, p) => new
ExamQuestionList()
{
Id = s.Id,
Question = s.Question,
CateTitle = "aaa",
Option = s.Option,
IsPass = s.IsPass,
Answer = s.Answer,
Difficulty = s.Difficulty,
IsDelete = s.IsDelete,
IsExist = true,
Score = p.Score,
CreateTime = s.CreateTime
})
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Question] AS [Question],
N'aaa' AS [C1],
[Extent1].[Option] AS [Option],
[Extent1].[IsPass] AS [IsPass],
[Extent1].[Answer] AS [Answer],
[Extent1].[Difficulty] AS [Difficulty],
[Extent1].[IsDelete] AS [IsDelete],
cast(1 as bit) AS [C2],
[Extent2].[Score] AS [Score],
[Extent1].[CreateTime] AS [CreateTime]
FROM [dbo].[ExamQuestions] AS [Extent1]
INNER JOIN [dbo].[PaperQuestions] AS [Extent2] ON [Extent1].[Id] = [Extent2].[QuestionId]
I user DefaulifEmpty but invalid, left outer join nest Outermost layer
var l = db.ExamQuestions
.Join(db.PaperQuestions, s => s.Id, p => p.QuestionId, (s, p) => new
ExamQuestionList()
{
Id = s.Id,
Question = s.Question,
CateTitle = "aaa",
Option = s.Option,
IsPass = s.IsPass,
Answer = s.Answer,
Difficulty = s.Difficulty,
IsDelete = s.IsDelete,
IsExist = true,
Score = p.Score,
CreateTime = s.CreateTime
}).DefaultIfEmpty();
SELECT
[Project1].[Id] AS [Id],
[Project1].[Question] AS [Question],
[Project1].[C1] AS [C1],
[Project1].[Option] AS [Option],
[Project1].[IsPass] AS [IsPass],
[Project1].[Answer] AS [Answer],
[Project1].[Difficulty] AS [Difficulty],
[Project1].[IsDelete] AS [IsDelete],
[Project1].[C2] AS [C2],
[Project1].[Score] AS [Score],
[Project1].[CreateTime] AS [CreateTime]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
LEFT OUTER JOIN (SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Question] AS [Question],
[Extent1].[Option] AS [Option],
[Extent1].[Answer] AS [Answer],
[Extent1].[CreateTime] AS [CreateTime],
[Extent1].[Difficulty] AS [Difficulty],
[Extent1].[IsPass] AS [IsPass],
[Extent1].[IsDelete] AS [IsDelete],
[Extent2].[Score] AS [Score],
N'aaa' AS [C1],
cast(1 as bit) AS [C2]
FROM [dbo].[ExamQuestions] AS [Extent1]
INNER JOIN [dbo].[PaperQuestions] AS [Extent2] ON [Extent1].[Id] = [Extent2].[QuestionId] ) AS [Project1] ON 1 = 1

It is a valid sql statement that it was generated by the linq provider and posted by you in the OP..
The most outer table [SingleRowTable1] allows to get always at least a single empty record composed by null fields named with the aliases specified in the statement by the column names prefixed with [Project1] table alias name.
The final join condition AS [Project1] ON 1 = 1 clause bind all rows getted by the inner most projection to the outer most fictitious table ( SELECT 1 AS X ) that get always 1 value annd attach it to each row getted by the innermost projection.
I hope it is a clear explanation..

Related

Infinite loop when I updated query to add an additional variable

I have this query:
with users_having_connected as (
select u.id as user_id,
(a.connected_at is not null) has_connected
from core_user u
join core_profile p on u.id = p.user_id
join core_conversation c on (c.profile1_id = p.id or c.profile2_id = p.id)
join analytics_connection a on c.id = a.conversation_id
group by u.id, (a.connected_at is not null)
)
select u.id as user_id,
date_trunc('month', u.created at time zone 'UTC')::date as month,
p.community_id,
p.organization_id,
p.profile_type_intention,
(p.basic_account_completed and (p.is_mentor or p.is_entrepreneur)) as profile_is_completed,
exists(select 1 from core_message where core_message.sender_id = p.id) as has_sent_a_message,
(EXISTS (SELECT 1 FROM core_message WHERE core_message.receiver_id = p.id)) AS has_received_a_message,
(EXISTS (SELECT 1 FROM core_admin_conversation_w_resp WHERE core_admin_conversation_w_resp.initiator_id = p.id or core_admin_conversation_w_resp.responder_id = p.id)) AS has_one_by_one,
exists(select 1 from users_having_connected where user_id = u.id) as has_two_by_two
from core_user as u
join core_profile p on u.id = p.user_id
where
p.profile_type_intention is not null
Which worked fine until I added this line:
(EXISTS (SELECT 1 FROM core_admin_conversation_w_resp WHERE core_admin_conversation_w_resp.initiator_id = p.id or core_admin_conversation_w_resp.responder_id = p.id)) AS has_one_by_one,
This specific variable add causes this query to infinite loop. What do I need to do to fix it? Am I missing a set of parentheses somewhere?

Querying for any entries after traversing multiple conditional includes in EF Core 5

Consider the following data model:
A Principal has a number of Roles (many-to-many);
Roles grant multiple Permissions (many-to-many);
Now i want to use LINQ to determine whether a Principle has a permission, i.e. whether he is in any Roles that possess this permission.
Usually I would go for AnyAsync on the join table for cases like this , but since I am traversing more than one join table I am really struggling. I initially came up with this:
var query = context.Principals
.Where(p => p.Id == "SomeUUID")
.Include(p => p.Roles)
.ThenInclude(r => r.Permissions
.Where(p => p.Name == "SomePermission"));
But now I would have to traverse, probably in-memory, the attached Roles through the Principal again to search for Any Permission.
Is there a way I can smoothly apply this check within the same query?
EDIT: Here is the generated SQL to complement Parks'(bottom) answer in comparison:
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Principals] AS [a]
INNER JOIN (
SELECT [a1].[Id], [a1].[Description], [a1].[DisplayName], [a0].[RoleId], [a0].[PrincipalId]
FROM [PrincipalRoles] AS [a0]
INNER JOIN [Roles] AS [a1] ON [a0].[RoleId] = [a1].[Id]
) AS [t] ON [a].[PrincipalId] = [t].[PrincipalId]
INNER JOIN (
SELECT [a3].[Id], [a3].[Name], [a2].[RoleId], [a2].[PermissionId]
FROM [RolePermissions] AS [a2]
INNER JOIN [Permissions] AS [a3] ON [a2].[PermissionId] = [a3].[Id]
) AS [t0] ON [t].[Id] = [t0].[RoleId]
WHERE ([a].[PrincipalId] = "SomePrincipal") AND ([t0].[Name] = "SomePermission")) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Principals] AS [a]
WHERE ([a].[PrincipalId] = "SomePrincipal") AND EXISTS (
SELECT 1
FROM [PrincipalRoles] AS [a0]
INNER JOIN [Roles] AS [a1] ON [a0].[RoleId] = [a1].[Id]
WHERE ([a].[PrincipalId] = [a0].[PrincipalId]) AND EXISTS (
SELECT 1
FROM [RolePermissions] AS [a2]
INNER JOIN [Permissions] AS [a3] ON [a2].[PermissionId] = [a3].[Id]
WHERE ([a1].[Id] = [a2].[RoleId]) AND ([a3].[Name] = "SomePermission")))) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
To see if a principal has a Role in Roles that has a Permission in Permissions you can use the following Where condition:
var result = await context.Principals
.Where(p => p.Id == "SomeUUID" && p.Roles.Any(r => r.Permissions.Any(x => x.Name == "SomePermission")))
.FirstOrDefaultAsync();
If result is null then either the Principal did not exist or did not have the permission you checked for. If result still needs to have the navigation properties then you can add the include statements originally included in your question.
Probably you are looking for this query:
var hasPermssion = context.Principals
.Where(p => p.Id == "SomeUUID")
.SelectMany(p => p.Roles)
.SelectMany(r => r.Permissions)
.Any(p => p.Name == "SomePermission");
Here is the generated SQL statement:
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Principals] AS [a]
INNER JOIN (
SELECT [a1].[Id], [a1].[Description], [a1].[DisplayName], [a0].[RoleId], [a0].[PrincipalId]
FROM [PrincipalRoles] AS [a0]
INNER JOIN [Roles] AS [a1] ON [a0].[RoleId] = [a1].[Id]
) AS [t] ON [a].[PrincipalId] = [t].[PrincipalId]
INNER JOIN (
SELECT [a3].[Id], [a3].[Name], [a2].[RoleId], [a2].[PermissionId]
FROM [RolePermissions] AS [a2]
INNER JOIN [Permissions] AS [a3] ON [a2].[PermissionId] = [a3].[Id]
) AS [t0] ON [t].[Id] = [t0].[RoleId]
WHERE ([a].[PrincipalId] = "SomePrincipal") AND ([t0].[Name] = "SomePermission")) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END

Entity Framework ... Four queries into one

I am using EF5 and finding questions as follows:
context.Questions
.OrderBy(x => Guid.NewGuid())
.Where(x => x.Difficulty == difficulty && x.Format == format);
Each Question has a field named duration with values (2, 4, 6 and 8) in minutes.
I have the following int array: { 4, 0, 1, 2 } which means:
Get 4 questions of 2 minutes, 0 of 4 minutes, 1 of 6 minutes and 2 of 8 minutes.
After .OrderBy and .Where I need to get 7 questions as mentioned.
Is there a way to do this without loading all questions or using 4 queries?
Thank You,
Miguel
This is kind of hardcoded - but you get the idea, and solves your problem explicitly. You should be able to convert it to whatever you need from here.
var questions = context.Questions .OrderBy(x => Guid.NewGuid())
.Where(x => x.Difficulty == difficulty && x.Format == format);
var selectedQuestions = questions.Where(q => q.Duration == 2).Take(questionArray[0])
.Union(questions.Where(q => q.Duration == 4).Take(questionArray[1]))
.Union(questions.Where(q => q.Duration == 6).Take(questionArray[2]))
.Union(questions.Where(q => q.Duration == 8).Take(questionArray[3]));
Since you're never enumerating the queryable, EF will do all of these unions in sql, and get all the data in a single call.
Produced SQL:
SELECT [Distinct3].[C1] AS [C1],
[Distinct3].[C2] AS [C2],
[Distinct3].[C3] AS [C3],
[Distinct3].[C4] AS [C4]
FROM (SELECT DISTINCT [UnionAll3].[C1] AS [C1],
[UnionAll3].[C2] AS [C2],
[UnionAll3].[C3] AS [C3],
[UnionAll3].[C4] AS [C4]
FROM (SELECT [Distinct2].[C1] AS [C1],
[Distinct2].[C2] AS [C2],
[Distinct2].[C3] AS [C3],
[Distinct2].[C4] AS [C4]
FROM (SELECT DISTINCT [UnionAll2].[C1] AS [C1],
[UnionAll2].[C2] AS [C2],
[UnionAll2].[C3] AS [C3],
[UnionAll2].[C4] AS [C4]
FROM (SELECT [Distinct1].[C1] AS [C1],
[Distinct1].[C2] AS [C2],
[Distinct1].[C3] AS [C3],
[Distinct1].[C4] AS [C4]
FROM (SELECT DISTINCT [UnionAll1].[Id] AS [C1],
[UnionAll1].[Duration] AS [C2],
[UnionAll1].[Difficulty] AS [C3],
[UnionAll1].[Format] AS [C4]
FROM (SELECT TOP (4) [Project1].[Id] AS [Id],
[Project1].[Duration] AS [Duration],
[Project1].[Difficulty] AS [Difficulty],
[Project1].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent1].[Id] AS [Id],
[Extent1].[Duration] AS [Duration],
[Extent1].[Difficulty] AS [Difficulty],
[Extent1].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent1]
WHERE ([Extent1].[Difficulty] = #p__linq__0)
AND ([Extent1].[Format] = #p__linq__1)
AND (2 = [Extent1].[Duration])) AS [Project1]
ORDER BY [Project1].[C1] ASC
UNION ALL
SELECT TOP (0) [Project3].[Id] AS [Id],
[Project3].[Duration] AS [Duration],
[Project3].[Difficulty] AS [Difficulty],
[Project3].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent2].[Id] AS [Id],
[Extent2].[Duration] AS [Duration],
[Extent2].[Difficulty] AS [Difficulty],
[Extent2].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent2]
WHERE ([Extent2].[Difficulty] = #p__linq__2)
AND ([Extent2].[Format] = #p__linq__3)
AND (4 = [Extent2].[Duration])) AS [Project3]
ORDER BY [Project3].[C1] ASC) AS [UnionAll1]) AS [Distinct1]
UNION ALL
SELECT TOP (1) [Project7].[Id] AS [Id],
[Project7].[Duration] AS [Duration],
[Project7].[Difficulty] AS [Difficulty],
[Project7].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent3].[Id] AS [Id],
[Extent3].[Duration] AS [Duration],
[Extent3].[Difficulty] AS [Difficulty],
[Extent3].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent3]
WHERE ([Extent3].[Difficulty] = #p__linq__4)
AND ([Extent3].[Format] = #p__linq__5)
AND (6 = [Extent3].[Duration])) AS [Project7]
ORDER BY [Project7].[C1] ASC) AS [UnionAll2]) AS [Distinct2]
UNION ALL
SELECT TOP (2) [Project11].[Id] AS [Id],
[Project11].[Duration] AS [Duration],
[Project11].[Difficulty] AS [Difficulty],
[Project11].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent4].[Id] AS [Id],
[Extent4].[Duration] AS [Duration],
[Extent4].[Difficulty] AS [Difficulty],
[Extent4].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent4]
WHERE ([Extent4].[Difficulty] = #p__linq__6)
AND ([Extent4].[Format] = #p__linq__7)
AND (8 = [Extent4].[Duration])) AS [Project11]
ORDER BY [Project11].[C1] ASC) AS [UnionAll3]) AS [Distinct3];
Why not just get 4 (or max of the array) questions of each type and do the filtering on the client? With 16 results it should be cheap - you can use linq grouping to group them by minutes and then it should be easy. Another interesting point here is ordering... If you have more than 4 questions but you always order them when selecting would you ever see any other questions that just first 4?
My query is a little bit more complex. Basically, I ended up with the following:
context.Questions
.OrderBy(x => Guid.NewGuid())
.Where(x =>
x.AccessLevel >= accessLevel &&
x.Enabled == true &&
x.QuestionFormat == questionFormat
x.Theme.Id == themeId
)
.Select(x => new {
Answers = x.Answers.Select(y => new {
Correct = y.Correct,
Packs = y.Packs.SelectMany(z => z.Files, (z, v) => new {
Id = z.Id,
File = new { Key = v.Key, Mime = v.Mime }
}),
Text = y.Text
}),
Duration = x.Duration,
Note = x.Note,
Text = x.Text,
Packs = x.Packs.SelectMany(y => y.Files, (y, z) => new {
Id = y.Id,
File = new { Key = z.Key, Mime = z.Mime }
})
})
.GroupBy(x => x.Duration);
So I am ordering the questions randomly and filtering them.
Then I get, for each question, its answers ...
Each question and answer have some files associated with them.
I have all files in one table. This is why I have Packs and Files in my query.
I am only loading the Files Keys and Mimes. Not the data.
So you suggestion is to get all questions grouped by time, right?
In fact, after do the AccessLevel / Enabled / QuestionFormat / Theme filtering I will never have so many questions ...
So I can group them by Duration as I did in the end of this query.
What would be a fast way to take the number of questions of each duration according to the array I posted in the beginning?
Thank You,
Miguel

Optimizing LINQ To Entities query

OK, let's say that I have two entities, A and B. For each user, there may be 0 or more A, with a unique combination of UserId (foreign key), GroupName, and Name properties. Entity A also has an "Value" property. Also for each user, there may be 0 or more B, with a UserID (again, a foreign key) and an "Occurred" property.
My task is to find all the B which have an Occurred property of more than 7 days ago OR have an Occurred property more than now - the number of hours in a particular A property. This code seems to work perfectly:
DateTime now = DateTime.UtcNow;
DateTime expired = now - TimeSpan.FromDays(7d);
using (DatabaseContext context = DatabaseContext.Create())
{
IQueryable<A> aQ = context.As.Where(a => a.GroupName == "Notifications" && s.Name == "Retention");
IQueryable<B> bQ = context.Bs.Where(
n => aQ.Any(a => a.UserId == b.UserId) ?
b.Occurred < EntityFunctions.AddHours(now, -aQ.FirstOrDefault(a => a.UserId == b.UserId).Value) :
b.Occurred < expired);
IList<B> bs = bQ.ToList();
// ...
}
This produces a SQL query along these lines:
SELECT
[Extent1].[ID] AS [ID],
[Extent1].[OCCURRED] AS [OCCURRED],
[Extent1].[USERID] AS [USERID],
FROM [dbo].[B] AS [Extent1]
OUTER APPLY (SELECT TOP (1)
[Extent2].[GROUPNAME] AS [GROUPNAME],
[Extent2].[NAME] AS [NAME],
[Extent2].[USERID] AS [USERID],
[Extent2].[VALUE] AS [VALUE],
FROM [dbo].[A] AS [Extent2]
WHERE (N'Notifications' = [Extent2].[GROUPNAME]) AND (N'Retention' = [Extent2].[NAME]) AND ([Extent2].[USERID] = [Extent1].[USERID]) ) AS [Element1]
OUTER APPLY (SELECT TOP (1)
[Extent3].[GROUPNAME] AS [GROUPNAME],
[Extent3].[NAME] AS [NAME],
[Extent3].[USERID] AS [USERID],
[Extent3].[VALUE] AS [VALUE],
FROM [dbo].[A] AS [Extent3]
WHERE (N'Notifications' = [Extent3].[GROUPNAME]) AND (N'Retention' = [Extent3].[NAME]) AND ([Extent3].[USERID] = [Extent1].[USERID]) ) AS [Element2]
WHERE (CASE WHEN ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[A] AS [Extent4]
WHERE (N'Notifications' = [Extent4].[GROUPNAME]) AND (N'Retention' = [Extent4].[NAME]) AND ([Extent4].[USERID] = [Extent1].[USERID])
)) THEN CASE WHEN ( CAST( [Extent1].[OCCURRED] AS datetime2) < (DATEADD (hours, -([Element1].[VALUE]), #p__linq__0))) THEN cast(1 as bit) WHEN ( NOT ( CAST( [Extent1].[OCCURRED] AS datetime2) < (DATEADD (hours, -([Element2].[VALUE]), #p__linq__0)))) THEN cast(0 as bit) END WHEN ([Extent1].[OCCURRED] < #p__linq__1) THEN cast(1 as bit) WHEN ( NOT ([Extent1].[OCCURRED] < #p__linq__1)) THEN cast(0 as bit) END) = 1
Please note that I've hacked the code and SQL query down from the actual stuff, so this may not be the perfect representation. But I hope it gets the point across: this looks a little hairy, at least in the way that the query is repeatedly checking A for matching groupname, name, and user. But I'm no Database expert. What I do know is that one observed execution ran roughly 2.5 seconds.
Is there a better way of going about this?
Thanks for any input!
---- SOLUTION ----
Thanks to Gert for this.
The code's query ended up like this:
var bQ =
from b in context.Bs
let offset = aQ.FirstOrDefault(a => a.UserId == b.UserId)
let expiration = (null != offset) ? EntityFunctions.AddHours(now, -offset.Value) : expired
where b.Occurred < expiration
select b;
Which is almost exactly what Gert suggested. The new SQL query looks like this:
SELECT
[Extent1].[ID] AS [ID],
[Extent1].[OCCURRED] AS [OCCURRED],
[Extent1].[USERID] AS [USERID]
FROM [dbo].[B] AS [Extent1]
OUTER APPLY (SELECT TOP (1)
[Extent2].[GROUPNAME] AS [GROUPNAME],
[Extent2].[NAME] AS [NAME],
[Extent2].[USERID] AS [USERID],
[Extent2].[VALUE] AS [VALUE]
FROM [dbo].[A] AS [Extent2]
WHERE (N'Notifications' = [Extent2].[GROUPNAME]) AND (N'Retention' = [Extent2].[NAME]) AND ([Extent2].[USERID] = [Extent1].[USERID]) ) AS [Element1]
WHERE CAST( [Extent1].[OCCURRED] AS datetime2) < (CASE WHEN ([Element1].[ID] IS NOT NULL) THEN DATEADD (hour, -([Element1].[VALUE]), #p__linq__0) ELSE #p__linq__1 END)
I think that this will query the A table only once:
from b in context.Bs
let offset = aQ.FirstOrDefault(a => a.UserId == b.UserId).Value
let dd = offset.HasValue
? EntityFunctions.AddHours(now, -offset)
: expired
where b.Occurred < dd

Can you use a SELECT INTO statement with a CTE that contains a UDF?

Can I do this:
With ZipCodeCTE as
{
select nvl(HH.GeoUSZip5 , **ZipCodeKeyLookUp**(HH.[CityName],HH.[StateName])) as TotalZipCode
from ODSDataArchive.archive.HHJob_Data_201202 HH
}
/* This Is a SELECT INTO statement that inserts
data into [Jobs].[dbo].[FactRPP]*/
SELECT [dbo].[FactJobsDaily].jobdid,
[dbo].[FactJobsDaily].DateKey,
[dbo].[FactJobsDaily].YearMonth,
[dbo].[FactJobsDaily].AccountKey,
[dbo].[FactJobsDaily].BridgeSocKey,
[dbo].[FactJobsDaily].HostSiteKey,
[dbo].[FactJobsDaily].JobClickedCount,
[dbo].[FactJobsDaily].JobResultsPageCount,
(select DZ.ZipCodeKey
from dimensions.dbo.DimZipCode DZ
where DZ.ZipCodeKey IN
(Select CAST(TotalZipCode AS INT)
from ZipCodeCTE))
INTO [Jobs].[dbo].[FactRPP]
from dbo.FactJobsDaily
inner join ODSDataArchive.archive.HHJob_Data_201202
on dbo.FactJobsDaily.JobDID = ODSDataArchive.archive.HHJob_Data_201202.DID
and dbo.FactJobsDaily.datekey = ODSDataArchive.archive.HHJob_Data_201202.datekey
inner join dimensions.dbo.Dimzipcode dzc
on ODSDataArchive.archive.HHJob_Data_201202.geoUSZip5 = dimensions.dbo.Dimzipcode.ZipCode
where [dbo].[FactJobsDaily].yearmonth= 201202
and [dbo].[FactJobsDaily].isactivekey = 1
-- and ODSDataArchive.archive.HHJob_Data_201202.geoUSZip5 <> ''
-- and ODSDataArchive.archive.HHJob_Data_201202.geoUSZip5 IS NOT NULL
and ODSDataArchive.archive.HHJob_Data_201202.status = 0
and ODSDataArchive.archive.HHJob_Data_201202.CountryName = 'US'
order by [dbo].[FactJobsDaily].jobdid;
Because the CTE translates into a regular query the short answer is yes.