Linq-to-sql не создает несколько внешних соединений? - PullRequest
7 голосов
/ 08 сентября 2011

У меня странная проблема с linq-to-sql, и я действительно пытался ее найти. Я проектирую базу данных sql и где только недавно пытался извлечь объект из нее.

Проблема с несколькими объединениями. Все мои таблицы используют в качестве первичных ключей столбцы идентификации.

ДБ разработан следующим образом:

MasterTable: Id (первичный ключ, столбец идентификаторов, int), MasterColumn1 (nvarchar (50))

Slave1: Id (первичный ключ, столбец идентификаторов, int), MasterId (int, первичный ключ -> Id MasterTable), SlaveCol1

Slave2: Id (первичный ключ, столбец идентификаторов, int), MasterId (int, первичный ключ -> Id MasterTable), SlaveColumn2

использованный код:

var db = new TestDbDataContext() { Log = Console.Out };
var res = from f in db.MasterTables
          where f.MasterColumn1 == "wtf"
          select new
                     {
                         f.Id, 
                         SlaveCols1 = f.Slave1s.Select(s => s.SlaveCol1),
                         SlaveCols2 = f.Slave2s.Select(s => s.SlaveColumn2)
                     };
foreach (var re in res)
{
    Console.Out.WriteLine(
        re.Id + " "
      + string.Join(", ", re.SlaveCols1.ToArray()) + " "
      + string.Join(", ", re.SlaveCols2.ToArray())
    );
}

И журнал:

SELECT [t0].[Id], [t1].[SlaveCol1], (
   SELECT COUNT(*)
   FROM [FR].[Slave1] AS [t2]
   WHERE [t2].[MasterId] = [t0].[Id]
   ) AS [value]
FROM [FR].[MasterTable] AS [t0]
LEFT OUTER JOIN [FR].[Slave1] AS [t1] ON [t1].[MasterId] = [t0].[Id]
WHERE [t0].[MasterColumn1] = @p0
ORDER BY [t0].[Id], [t1].[Id]
-- @p0: Input NVarChar (Size = 3; Prec = 0; Scale = 0) [wtf]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
SELECT [t0].[SlaveColumn2]
   FROM [FR].[Slave2] AS [t0]
   WHERE [t0].[MasterId] = @x1
-- @x1: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
1 SlaveCol1Wtf SlaveCol2Wtf

Почему бы не сделать два внешних соединения? Я действительно беспокоюсь об этом, потому что у меня есть намного большая база данных со многими таблицами, ссылающимися на одну и ту же таблицу (все имеют отношение один ко многим), и наличие 20 выборок циклических обращений к серверу базы данных не является оптимальным!

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

var db = new TestDbDataContext() { Log = Console.Out };
var res = from f in db.MasterTables
          join s1 in db.Slave1s on f.Id equals s1.MasterId into s1Tbl
          from s1 in s1Tbl.DefaultIfEmpty()
          join s2 in db.Slave2s on f.Id equals s2.MasterId into s2Tbl
          from s2 in s2Tbl.DefaultIfEmpty()
          where f.MasterColumn1 == "wtf"
          select new { f.Id, s1.SlaveCol1, s2.SlaveColumn2 };
foreach (var re in res)
{
    Console.Out.WriteLine(re.Id + " " + re.SlaveCol1 + " " + re.SlaveColumn2);
}

Но я хочу использовать ссылки, которые предоставляет Linq-To-Sql, а не ручные соединения! Как?

----------- редактировать -----------------

Я также пытался выполнить предварительную выборку так:

using (new DbConnectionScope())
{
    var db = new TestDbDataContext() { Log = Console.Out };
    DataLoadOptions loadOptions = new DataLoadOptions();
    loadOptions.LoadWith<MasterTable>(c => c.Slave1s);
    loadOptions.LoadWith<MasterTable>(c => c.Slave2s);
    db.LoadOptions = loadOptions;

    var res = from f in db.MasterTables
              where f.MasterColumn1 == "wtf"
              select f;
    foreach (var re in res)
    {
        Console.Out.WriteLine(re.Id + " " + 
            string.Join(", ", re.Slave1s.Select(s => s.SlaveCol1).ToArray()) + " " + 
            string.Join(", ", re.Slave2s.Select(s => s.SlaveColumn2).ToArray()));
    }
}

тот же результат = (

Ответы [ 4 ]

1 голос
/ 08 сентября 2011

Вы находитесь на правильном пути с опцией предварительной выборки с использованием LoadOptions и обходом ассоциаций, а не явных объединений, однако, поскольку вы пытаетесь выполнить несколько переходов 1-M из вашей MasterTable, вы фактически создадите декартово произведение записи Slave1 и Slave2. В результате LINQ to SQL игнорирует ваши параметры загрузки и лениво загружает дочерние записи для каждого из ваших детей.

Вы можете немного оптимизировать это, удалив опцию второй дочерней нагрузки. Сгенерированный запрос теперь выполняет один запрос, возвращая ваши MasterTable и Slave1s, но затем лениво загружает каждый из Slave2s. Вы должны увидеть то же самое, если сделаете следующее:

var res = from f in db.MasterTables
          where f.MasterColun1 == "wtf"
          select new 
          {
             f.Id,
             Cols1 = f.Slave1s.Select(s => s.SlaveCol1).ToArray()
             Cols2 = f.Slave2s.Select(s => s.SlaveColumn2).ToArray()
          }

Вы должны увидеть левое соединение между MasterTables и Slave1s, а затем ленивую загрузку Slave2s, чтобы избежать декартового произведения между Slave1 и Slave2 в сглаженных результатах SQL.

1 голос
/ 08 сентября 2011

Что касается «почему», Linq-to-SQL, вероятно, думает, что он делает ваш запрос лучше, избегая множественных внешних объединений.

Допустим, вы извлекаете 20 записей из основной таблицы, и каждая подчиненная таблица имеет 20 записей на запись в основной таблице. Вы потянете 8000 входов через провод за один круговой обход с внешним соединением, в отличие от двух круговых рейсов с 400 за штуку. Наступает момент, когда дешевле совершить две поездки в оба конца. Это может быть неверно в данном конкретном случае, но есть большая вероятность, что если вы объедините таким образом очень много таблиц и если вы извлечете много данных для каждой таблицы, это может очень легко перевесить шкалы.

Возможно, вы также захотите рассмотреть возможность того, что LINQ to SQL может выполнять оба SELECT за один прием, используя несколько наборов результатов. В этом случае подход с двумя утверждениями, вероятно, будет намного быстрее, чем двойное внешнее соединение.

Обновление

После небольшого тестирования становится очевидным, что ответ Джима Вули более правильный: очевидно, Linq to SQL просто решает не загружать с нетерпением ничего, кроме первого указанного вами свойства. Это тоже странно, потому что это не совсем ленивая загрузка. Он загружает каждое свойство в отдельном цикле как часть первоначальной оценки вашего запроса. Мне кажется, это довольно существенное ограничение LINQ to SQL.

0 голосов
/ 08 сентября 2011

Попробуйте:

var res = from f in db.MasterTables
          where f.MasterColumn1 == "wtf"
          let s1 = f.Slave1s.Select(s => s.SlaveCol1)
          let s2 = f.Slave2s.Select(s => s.SlaveColumn2)
          select new {
                         f.Id, 
                         SlaveCols1 = s1,
                         SlaveCols2 = s2
                     };
0 голосов
/ 08 сентября 2011

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

Я думаю, вам нужно выполнить предварительную загрузку этих объединенных таблиц.

Смотрите эти ссылки:

...