Структура сущностей и принудительное внутреннее объединение

#c#-4.0 #linq-to-entities #entity-framework-4.1

#c #-4.0 #linq-to-entities #entity-framework-4.1

Вопрос:

У меня есть Table1 со следующими связями (они не применяются, они только создают связь для свойств навигации)

Таблица1 (*)-> (1) Таблица2
Таблица1 (*)-> (1) Таблица3
Таблица1 (*)-> (1) Таблица4
Таблица1 (*)-> (1) Таблица5

Использование кода с активной загрузкой выглядит следующим образом

 IQueryable<Table1> query = context.Table1s;

query = query.Include(Table1 => Table1.Table2);
query = query.Include(Table1 => Table1.Table3);
query = query.Include(Table1 => Table1.Table4);
query = query.Include(Table1 => Table1.Table5);

query = query.Where(row => row.Table1Id == table1Id);

query.Single();
  

Каждый раз, когда я пытаюсь организовать операторы Include(), первая включенная таблица имеет внутреннее соединение в сгенерированном TSQL, а остальные являются Left Outer Join (я ожидаю, что для всех из них Left Outer ). Я не разделяю сущности, это просто простые таблицы с FKS.

Если DefaultIfEmpty() является единственным решением, может ли кто-нибудь объяснить причину, по которой, когда все, кроме первой включенной таблицы, предоставляют ожидаемый SQL?

Я понимаю, что поведение по умолчанию для свойства навигации остается ВНЕШНИМ, но я не могу получить ВСЕ свойства для создания значения по умолчанию.

Любая помощь будет высоко оценена.

Заранее благодарю вас!

—— Создан TSQL (изменен для краткости, но структура та же) ——-

(@p__linq__0 int)ВЫБЕРИТЕ 
[Ограничение1].[Table1Id] КАК [Table1Id], 
[Ограничение1].[OtherData] КАК [OtherData]
ИЗ (ВЫБЕРИТЕ TOP (2) 
 [Extent1].[Table1Id] КАК [Table1Id], 
 [Extent1].[OtherData] Как [OtherData]
 ИЗ [dbo].[Table1] КАК [Extent1]
 ВНУТРЕННЕЕ СОЕДИНЕНИЕ [dbo].[Table2] КАК [Extent2] НА [Extent1].[Table2Id] = [Extent2].[Table2Id]
 ЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ [dbo].[Table3] КАК [Extent3] НА [Extent1].[Table3Id] = [Extent3].[Table3Id]
 ЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ [dbo].[Table4] КАК [Extent4] НА [Extent1].[Table4Id] = [Extent4].[Table4Id]
 ЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ [dbo].[Table5] КАК [Extent5] НА [Extent1].[Table5Id] = [Extent5].[Table5Id]
 ГДЕ [Extent1] .[Table1Id] = @p__linq__0
) КАК [Limit1]

Комментарии:

1. В чем проблема? Разве запрос не возвращает ожидаемый результат со всеми включенными объектами? Я получаю то же самое с двумя включениями: внутренним соединением и внешним соединением (и объединением обоих). Я никогда этого не замечал, но я не вижу неправильного результата.

2. Мы не применяем принудительное существование данных в Table2, поэтому внутреннее объединение приводит к пустому результату при поиске Table1Id с допустимым идентификатором. Ни в одной из таблиц не применяются принудительные отношения, поэтому внутреннее соединение при ЛЮБОЙ из ожидаемых загрузок вызовет «Последовательность не найдена», если только соответствующие данные не должны были быть помещены в запрос для объединенной таблицы.

3. Почему он выполняет выбор TOP 2? Это кажется странным.

4. @mattruma: ПРИ использовании всегда создается SELECT TOP 2 Single (чтобы проверить, есть ли более 1 записи). Это нормально.

5. Если вы не применяете данные к Table2, вы делаете это неправильно, потому что в вашем отношении указано, что каждая запись в Table1 должна иметь связанную запись в Table2.

Ответ №1:

EF, похоже, используется INNER JOIN для включения требуемого и LEFT OUTER JOIN для включения необязательного свойства навигации. Пример:

 public class Order
{
    public int Id { get; set; }
    public string Details { get; set; }
    public Customer Customer { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}
  

Если я определяю Customer как обязательное свойство на Order

 public class MyContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>()
            .HasRequired(o => o.Customer)
            .WithMany();
    }
}
  

… и выполните этот запрос…

 using (var ctx = new MyContext())
{
    var result = ctx.Orders
        .Include(o => o.Customer)
        .Where(o => o.Details == "Peanuts")
        .FirstOrDefault();
}
  

… Я получаю этот SQL:

 SELECT TOP (1) 
[Extent1].[Id] AS [Id], 
[Extent1].[Details] AS [Details], 
[Extent2].[Id] AS [Id1], 
[Extent2].[Name] AS [Name]
FROM  [dbo].[Orders] AS [Extent1]
INNER JOIN [dbo].[Customers] AS [Extent2] 
    ON [Extent1].[Customer_Id] = [Extent2].[Id]
WHERE N'Peanuts' = [Extent1].[Details]
  

Если я изменю конфигурацию модели .HasRequired(o => o.Customer) на…

 .HasOptional(o => o.Customer)
  

… Я получаю точно такой же запрос, за исключением того, что INNER JOIN [dbo].[Customers] AS [Extent2] заменяется:

 LEFT OUTER JOIN [dbo].[Customers] AS [Extent2]
  

С точки зрения модели это имеет смысл, потому что вы говорите, что никогда не может быть an Order без a Customer , если вы определяете отношения по мере необходимости. Если вы обойдете это требование, удалив принудительное исполнение в базе данных, и если у вас действительно есть заказы без клиента, вы нарушаете свое собственное определение модели.

Единственное решение, вероятно, сделает отношения необязательными, если у вас такая ситуация. Я не думаю, что можно управлять SQL, который создается при использовании Include .

Комментарии:

1. Slauma спасибо за пример. Жаль, что я сначала не использовал код.

Ответ №2:

в EF при выполнении IQueryable.Include() , если ни одно из свойств навигации не основано на принудительном отношении, тогда EF будет использовать первую таблицу. Он ожидает, что по крайней мере одно из отношений будет применено в схеме и что одно из них должно быть закодировано с IQueryable.Include() первым, а затем добавить другие таблицы с Include()

Комментарии:

1. Не могли бы вы дать ссылку на документацию, которая подтверждает это, пожалуйста?

Ответ №3:

Как заставить Entity Framework выполнять внутренние объединения, если у вас есть структура таблицы, такая, что:

  • Учащиеся могут иметь расписания, но не обязаны
  • Расписания могут иметь классы, но не обязательно
  • Классы ДОЛЖНЫ иметь учебные программы
  • Учебные программы ДОЛЖНЫ иметь тесты

Если вы хотите найти студентов, которые прошли определенные тесты, логично было бы сделать что-то вроде:

 var studentsWhoPassed = context.Set<StudentEntity>()
    .Where(x => x.Something)
    .Include(x => x.Schedules.Select(y => y.Classes.Select(z => z.Tests)))
    .Etc().Etc()
  

Суть в том, что вы начинаете с StudentEntity и вводите некоторые условия, основанные на объединениях по цепочке. Но поскольку учащийся для планирования является необязательным, E.F. генерирует ВНЕШНИЕ соединения СЛЕВА.

Вместо этого вам следует начать с более низкого уровня цепочки и наращивать. Например:

 var studentsWhoPassed = context.Set<ClassEntity>()
    .Where(class => class.Tests.Any(test => test.Status == Status.Passed)
        amp;amp; class.Schedule.Student.Something == studentSomething)
    .Include(class => class.Schedule.Student)
  

Странно начинать с класса, когда вы пытаетесь запросить учащихся с критериями тестирования. Но на самом деле это упрощает LINQ.

Поскольку у студента не должно быть расписания, но … у класса должны быть тесты, и у класса должен быть ScheduleId, а у расписаний должен быть StudentID, вы получаете внутренние соединения повсюду.

Конечно, этот школьный пример абстрактен, но идея верна другим примерам, с которыми я работал с теми же типами отношений.