Могу ли я заставить автоматически сгенерированные классы Linq-to-SQL использовать OUTER JOIN? - PullRequest
2 голосов
/ 15 марта 2010

Допустим, у меня есть таблица Order с полем FirstSalesPersonId и полем SecondSalesPersonId. Оба они являются внешними ключами, которые ссылаются на таблицу SalesPerson. Для любого данного заказа, один или два продавца могут быть зачислены на заказ. Другими словами, FirstSalesPersonId никогда не может быть NULL, но SecondSalesPersonId может быть NULL.

Когда я перетаскиваю свои таблицы Order и SalesPerson на поверхность конструирования "Linq to SQL Classes", конструктор классов обнаруживает две зависимости FK от таблицы Order до таблицы SalesPerson, и поэтому Сгенерированный класс Order имеет поле SalesPerson и поле SalesPerson1 (которое я могу переименовать в SalesPerson1 и SalesPerson2, чтобы избежать путаницы).

Поскольку я всегда хочу, чтобы данные продавца были доступны при обработке заказа, я использую DataLoadOptions.LoadWith, чтобы указать, что два поля продавца заполняются при заполнении экземпляра заказа, следующим образом:

dataLoadOptions.LoadWith<Order>(o => o.SalesPerson1);
dataLoadOptions.LoadWith<Order>(o => o.SalesPerson2);

Проблема У меня возникло то, что Linq to SQL использует что-то вроде следующего SQL для загрузки ордера:

SELECT ...
FROM Order O
INNER JOIN SalesPerson SP1 ON SP1.salesPersonId = O.firstSalesPersonId
INNER JOIN SalesPerson SP2 ON SP2.salesPersonId = O.secondSalesPersonId

Это имело бы смысл, если бы всегда было две записи продавца, но, поскольку иногда нет второго продавца (secondSalesPersonId равен NULL), INNER JOIN заставляет запрос не возвращать записи в этом случае.

Что я действительно хочу здесь, так это изменить второй INNER JOIN на LEFT OUTER JOIN. Есть ли способ сделать это через пользовательский интерфейс для генератора классов? Если нет, то как еще можно этого добиться?

(Обратите внимание, что, поскольку я использую сгенерированные классы почти исключительно, я бы предпочел, чтобы в этом одном случае не было прикреплено что-либо, если бы я мог этого избежать).


Редактировать: в ответ на мой комментарий поле SecondSalesPersonId имеет значение , допускающее значение NULL (в БД и в сгенерированных классах).

Ответы [ 2 ]

1 голос
/ 18 марта 2010

Поведение по умолчанию равно a LEFT JOIN, при условии, что вы правильно настроили модель.

Вот немного анонимный пример, который я только что протестировал на одной из моих собственных баз данных:

class Program
{
    static void Main(string[] args)
    {
        using (TestDataContext context = new TestDataContext())
        {
            DataLoadOptions dlo = new DataLoadOptions();
            dlo.LoadWith<Place>(p => p.Address);
            context.LoadOptions = dlo;

            var places = context.Places.Where(p => p.ID >= 100 && p.ID <= 200);
            foreach (var place in places)
            {
                Console.WriteLine(p.ID, p.AddressID);
            }
        }
    }
}

Это простой тест, который выводит список мест и их идентификаторы адресов. Вот текст запроса, который появляется в профилировщике:

SELECT [t0].[ID], [t0].[Name], [t0].[AddressID], ...
FROM [dbo].[Places] AS [t0]
LEFT OUTER JOIN (
    SELECT 1 AS [test], [t1].[AddressID],
        [t1].[StreetLine1], [t1].[StreetLine2],
        [t1].[City], [t1].[Region], [t1].[Country], [t1].[PostalCode]
    FROM [dbo].[Addresses] AS [t1]
) AS [t2] ON [t2].[AddressID] = [t0].[AddressID]
WHERE ([t0].[PlaceID] >= @p0) AND ([t0].[PlaceID] <= @p1)

Это не совсем симпатичный запрос (ваше предположение так же хорошо, как и мое, с точки зрения того, что означает 1 as [test]), но он определенно является LEFT JOIN и не представляет проблемы, которую вы, похоже, представляете иметь. И это только сгенерированные классы, я не внес никаких изменений.

Обратите внимание, что я также проверил это на двойных отношениях (то есть на единственном Place, имеющем две ссылки Address, одна обнуляемая, другая нет), и я получаю точно такие же результаты. Первый (не обнуляемый) превращается в INNER JOIN, а второй превращается в LEFT JOIN.

Это должно быть что-то в вашей модели, например, изменение обнуляемости второй ссылки. Я знаю, вы говорите, что он настроен как обнуляемый, но, возможно, вам нужно перепроверить? Если он определенно обнуляемый, я предлагаю вам опубликовать полную схему и DBML, чтобы кто-нибудь мог попытаться воспроизвести то поведение, которое вы видите.

0 голосов
/ 15 марта 2010

Если вы сделаете поле secondSalesPersonId в таблице базы данных обнуляемым, LINQ-to-SQL должен правильно построить объект Association, чтобы результирующий оператор SQL выполнил LEFT OUTER JOIN.

UPDATE: Поскольку поле может быть пустым, ваша проблема может быть в явном объявлении dataLoadOptions.LoadWith<>(). У меня похожая ситуация в моем текущем проекте, где у меня есть Заказ, но заказ проходит несколько этапов. Каждый этап соответствует отдельной таблице с данными, относящимися к этому этапу. Я просто получаю Заказ, и соответствующие данные следуют, если он существует. Я вообще не использую dataLoadOptions, и он делает то, что мне нужно. Например, если в Заказе есть запись заказа на покупку, но нет записи счета-фактуры, Order.PurchaseOrder будет содержать данные заказа на покупку, а Order.Invoice будет нулевым. Мой запрос выглядит примерно так:

DC.Orders.Where(a => a.Order_ID == id).SingleOrDefault();

Я стараюсь не управлять LINQ-to-SQL на микроуровне ... он делает 95% того, что мне нужно, прямо из коробки.

ОБНОВЛЕНИЕ 2: Я нашел этот пост , в котором обсуждается использование DefaultIfEmpty () для заполнения дочерних сущностей нулем, если они не существуют. Я опробовал его с помощью LINQPad в своей базе данных и преобразовал этот пример в лямбда-синтаксис (поскольку я это и использую):

ParentTable.GroupJoin
(
    ChildTable, 
    p => p.ParentTable_ID, 
    c => c.ChildTable_ID, 
    (p, aggregate) => new { p = p, aggregate = aggregate }
)
.SelectMany (a => a.aggregate.DefaultIfEmpty (), 
    (a, c) => new 
    { 
        ParentTableEntity = a.p, 
        ChildTableEntity = c
    }
)

Из того, что я могу понять из этого оператора, выражение GroupJoin связывает родительскую и дочернюю таблицы, а выражение SelectMany объединяет связанные дочерние записи. Похоже, что ключом является использование DefaultIfEmpty, которое принудительно включает запись родительской сущности, даже если нет связанных дочерних записей. (Спасибо, что заставили меня углубиться в это ... Я думаю, я мог бы найти кое-что полезное, чтобы помочь с довольно большим отчетом, который я получил на своем конвейере ...)

ОБНОВЛЕНИЕ 3: Если цель состоит в том, чтобы сделать его простым, то похоже, что вам придется ссылаться на эти поля продавца непосредственно в выражении Select (). Причина, по которой вам в первую очередь нужно использовать LoadWith <> (), заключается в том, что на таблицы нет ссылок нигде в вашем запросе, поэтому механизм LINQ не будет автоматически извлекать эту информацию.

В качестве примера приведена такая структура:

MailingList               ListCompany
===========               ===========
List_ID (PK)              ListCompany_ID (PK)
ListCompany_ID (FK)       FullName (string)

Я хочу получить название компании, связанной с определенным списком рассылки:

MailingLists.Where(a => a.List_ID == 2).Select(a => a.ListCompany.FullName)

Если эта связь НЕ была создана, то есть поле ListCompany_ID в таблице MailingList для этой записи равно null, это результирующий SQL, сгенерированный механизмом LINQ:

SELECT [t1].[FullName]
FROM [MailingLists] AS [t0]
LEFT OUTER JOIN [ListCompanies] AS [t1] ON [t1].[ListCompany_ID] = [t0].[ListCompany_ID]
WHERE [t0].[List_ID] = @p0
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...