9

Consider i have below entities:

    public class Root
    {
        public long Id { get; set; }
    }

    public class School : Root
    {
        public long StudentId { get; set; }
        public Student Student { get; set; }
        public Teacher Teacher { get; set; }
        public long TeacherId { get; set; }
    }

    public class Student : Root
    {
    }

    public class Teacher : Root
    {
    }

Now, after this fix in EF i can build left join query like this:

ctx.Schools
    .GroupJoin(ctx.Teachers, school => school.TeacherId, teacher => teacher.Id,
        (school, teachers) => new { school, teachers })
    .SelectMany(info => info.teachers.DefaultIfEmpty(),
        (info, teacher) => new { info.school, teacher })
    .Where(info => info.school.Id == someSchoolId)
    .Select(r => r.school);

or like this:

from school in ctx.Schools
    join teacher in ctx.Teachers on school.TeacherId equals teacher.Id into grouping
    from t in grouping.DefaultIfEmpty()
    where school.Id == someSchoolId
    select school;

The sql produced is:

SELECT [school].[Id], [school].[StudentId], [school].[TeacherId], [teacher].[Id]
FROM [Schools] AS [school]
LEFT JOIN [Teachers] AS [teacher] ON [school].[TeacherId] = [teacher].[Id]
WHERE [school].[Id] = @__someSchoolId_0
ORDER BY [school].[TeacherId]

But(!), when i try to add one more table to left join

ctx.Schools
    .GroupJoin(ctx.Teachers, school => school.TeacherId, teacher => teacher.Id,
        (school, teachers) => new { school, teachers })
    .SelectMany(info => info.teachers.DefaultIfEmpty(),
        (info, teacher) => new { info.school, teacher })
    .GroupJoin(ctx.Students, info => info.school.StudentId, student => student.Id,
        (info, students) => new {info.school, info.teacher, students})
    .SelectMany(info => info.students.DefaultIfEmpty(), 
        (info, student) => new {info.school, info.teacher, student})
    .Where(data => data.school.Id == someSchoolId)
    .Select(r => r.school);

or

from school in ctx.Schools
    join teacher in ctx.Teachers on school.TeacherId equals teacher.Id into grouping
    from t in grouping.DefaultIfEmpty()
    join student in ctx.Students on school.StudentId equals student.Id into grouping2
    from s in grouping2.DefaultIfEmpty()
    where school.Id == someSchoolId
    select school;

Threre two separate sql queries produced:

SELECT [student].[Id]
FROM [Students] AS [student]

SELECT [school].[Id], [school].[StudentId], [school].[TeacherId], [teacher].[Id]
FROM [Schools] AS [school]
LEFT JOIN [Teachers] AS [teacher] ON [school].[TeacherId] = [teacher].[Id]
WHERE [school].[Id] = @__someSchoolId_0
ORDER BY [school].[TeacherId]

Looks like there are client-side left join appears.

What am i doing wrong?

dimitri
  • 131
  • 1
  • 1
  • 6
  • You are doing nothing wrong (left joins with your model make no sense because the fields are required, but that's not essential - the same happens if you make them optional). It's the current EF Core fault. – Ivan Stoev Aug 07 '16 at 18:30

1 Answers1

9

You need to select from all the 3 tables so that the left joins would make sense when the Entity Framework translates from the Linq AST to SQL

select new { school, t, s };

instead of

select school;

Then, if you check in Debug from Visual Studio during the program execution and you copy the value of the query to the clipboard, you'll find - as expected - 2 LEFT OUTER JOINs after the FROM

Errata Corrige

The 2 left outer joins were visible from EF 6.

EF Core logger writes that the query ...

could not be translated and will be evaluated locally.

The only remark here is that - without selecting the other tables - there would have been no reasons to find multiple left joins in the first place

EF Core design

Based on the unit testing seen in the github repo and trying to closer meet the OP requirements, I would suggest the following query

var querySO = ctx.Schools
        .Include(x => x.Student)
        .Include(x => x.Teacher)
        ;

var results = querySO.ToArray();

This time I see a couple of LEFT JOIN from EF Core Logger

PRAGMA foreign_keys=ON;Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']

SELECT "x"."SchoolId", "x"."StudentId", "x"."TeacherId", "s"."StudentId", "s"."name", "t"."TeacherId", "t"."name"

FROM "Schools" AS "x"

LEFT JOIN "Students" AS "s" ON "x"."StudentId" = "s"."StudentId"

LEFT JOIN "Teachers" AS "t" ON "x"."TeacherId" = "t"."TeacherId"

having defined a model

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<School>().HasKey(p => p.SchoolId);
    modelBuilder.Entity<Teacher>().HasKey(p => p.TeacherId);
    modelBuilder.Entity<Student>().HasKey(p => p.StudentId);


    modelBuilder.Entity<School>().HasOne<Student>(s => s.Student)
        .WithOne().HasForeignKey<School>(s => s.StudentId);
    modelBuilder.Entity<School>().HasOne<Teacher>(s => s.Teacher)
        .WithOne().HasForeignKey<School>(s => s.TeacherId);

}

and classes

public class School 
{
    public long SchoolId { get; set; }
    public long? StudentId { get; set; }
    public Student Student { get; set; }
    public Teacher Teacher { get; set; }
    public long? TeacherId { get; set; }
}

public class Student 
{
    public long StudentId { get; set; }
    public string name { get; set; }
}

public class Teacher 
{
    public long TeacherId { get; set; }
    public string name { get; set; }
}
  • Thank you for your answer! But this is does not help me. The query with your fix: (from school in ctx.Schools join teacher in ctx.Teachers on school.TeacherId equals teacher.Id into grouping from t in grouping.DefaultIfEmpty() join student in ctx.Students on school.StudentId equals student.Id into grouping2 from s in grouping2.DefaultIfEmpty() where school.Id == someSchoolId select new {school, t, s}).ToArray().Select(c=>c.school); – dimitri Aug 07 '16 at 16:24
  • @dimitri 1) Are you able to understand that a left join is useless and will be ignored if you don't select fields from the other table? 2) Are you able to understand that the SQL is produced only when the query is enumerated and to spot which lines of code are related to which SQL? –  Aug 07 '16 at 18:07
  • @MachineLearning While you are right in general, the above doesn't work in EF Core. EF Core and left outer joins - it had problems with them in RCs, and still have them in RTM. – Ivan Stoev Aug 07 '16 at 18:19
  • @MachineLearning, wich part of my answer led you to such questions? Didnt you see that i execute enumeration in my comment? (.ToArray() method) And also before execution of enumeration i add ".select new {school, t, s}" as you suggested. – dimitri Aug 07 '16 at 18:25
  • @Dimitri We all did. But that's what we've got:( – Ivan Stoev Aug 07 '16 at 18:45
  • @Ivan Stoev while you're usually very technical in your answers, here you're adding more vague comments... I've opened an EF project before answering and I've described what I've done and seen while the OP is replying without any specific SQL and obviously it can be the same. BTW I don't want to discuss this thing at the level of a github repo issue/commit... –  Aug 07 '16 at 19:15
  • @MachineLearning Actually I'm using the official EF Core package and I **do** observe the behavior described by OP, applying your recommendation and also a couple other attempts. Even using just navigation properties. And btw, in EF Core you cannot see the generated SQL as in EF6. I had to activate EF Core logging in order to see it (along with other things) in the Console window. – Ivan Stoev Aug 07 '16 at 19:26
  • @MachineLearning Please don't get me wrong. I'm just quite disappointed by EF Core RTM :) – Ivan Stoev Aug 07 '16 at 19:36
  • @IvanStoev you're right, I was looking at EF6. EF Core logger writes "... could not be translated and will be evaluated locally" but it's not that bad imho ... it has multiple data providers –  Aug 07 '16 at 22:17
  • @dimitri I have completed my answer now –  Aug 08 '16 at 17:25
  • @IvanStoev what do you think about my proposed solution with EF Core includes? –  Aug 08 '16 at 17:25
  • 1
    Yes, it works (I checked that yesterday), but it's just eager loading entities and not a general purpose query. So as you can see, they can do `left joins` with includes, but can't when you explicitly ask via query - huh. I'm pretty sure all that will be fixed, but after a couple sub releases. If we are lucky, in 1.1 :) Anyway, +1 by me for still fighting the issue. – Ivan Stoev Aug 08 '16 at 17:35
  • 1
    @IvanStoev yes, your comments inspired me to go on, thanks –  Aug 08 '16 at 17:38
  • @MachineLearning, you right, navigation properties and "Include" solving described problem. But, as Ivan noted, there is no way to build left join explicitly. I just [posted](http://stackoverflow.com/questions/38861682/how-to-build-several-left-join-query-in-entity-framework-core-for-several-many-t) more precise question related to my real problem. Could you please look at it. – dimitri Aug 09 '16 at 23:04
  • In case anyone stops by and sees the link, as of EF Core 6.0+ this is corrected and the OP query with multi GroupJoins or query format do correctly produce the two left joins, and also if only checking a value from the root DbSet – Nathan Teague Apr 17 '23 at 14:38