Загрузка связанных данных EF

95
10 апреля 2022, 20:30

Имеется таблица Tickets, у которой несколько связей. Чтобы загрузить все необходимые связанные данные, получается такой запрос:

var ticket = _db.Tickets
            .Include(t=>t.Group)
            .Include(t=>t.TicketContacts)
            .ThenInclude(t=>t.Contact)
            .Include(t=>t.TicketProducts)
            .ThenInclude(t=>t.Product)
            .Include(t=>t.AppUser)
            .Include(t => t.Status)
            .Include(t => t.Abonent)
            .Include(t=>t.Events)
            .ThenInclude(t=>t.Message)
            .ThenInclude(t=>t.Attachments)
            .Include(t => t.Events)
            .ThenInclude(t => t.Message)
            .ThenInclude(t => t.Contact)
            .Include(t=>t.Events)
            .ThenInclude(t=>t.User)
            .FirstOrDefault(t => t.Id == id);

Настораживает большое количество include'ов. В бд будут отправляться несколько запросов. Правильно ли я делаю, или можно как то это упростить ?

Как я понял (по окну Вывода), получаю 5 запросов:

SELECT TOP(1) [t].[Id], ...
FROM[Tickets] AS[t]
LEFT JOIN[Abonents] AS[t.Abonent] ON[t].[AbonentId] = [t.Abonent].[Id]
INNER JOIN[Statuses] AS[t.Status] ON[t].[StatusId] = [t.Status].[Id]
LEFT JOIN[AspNetUsers] AS[t.AppUser] ON[t].[AppUserId] = [t.AppUser].[Id]
LEFT JOIN[Groups] AS[t.Group] ON[t].[GroupId] = [t.Group].[Id]
WHERE[t].[Id] = @__id_0
ORDER BY[t].[Id]
SELECT[t.TicketContacts].[TicketId], ...
FROM[TicketContacts] AS[t.TicketContacts]
INNER JOIN[Contacts] AS[t.Contact] ON[t.TicketContacts].[ContactId] =  [t.Contact].[Id]
INNER JOIN(
SELECT DISTINCT [t1].*
FROM (
    SELECT TOP(1) [t0].[Id]
    FROM[Tickets] AS[t0]
    LEFT JOIN[Abonents] AS[t.Abonent0] ON[t0].[AbonentId] = [t.Abonent0].[Id]
    INNER JOIN[Statuses] AS[t.Status0] ON[t0].[StatusId] = [t.Status0].[Id]
    LEFT JOIN[AspNetUsers] AS[t.AppUser0] ON[t0].[AppUserId] = [t.AppUser0].[Id]
    LEFT JOIN[Groups] AS[t.Group0] ON[t0].[GroupId] = [t.Group0].[Id]
    WHERE[t0].[Id] = @__id_0
    ORDER BY[t0].[Id]
) AS[t1]
) AS[t2] ON[t.TicketContacts].[TicketId] = [t2].[Id]
ORDER BY[t2].[Id]
SELECT[t.TicketProducts].[TicketId], ...
FROM[TicketProducts] AS[t.TicketProducts]
INNER JOIN[Products] AS[t.Product] ON[t.TicketProducts].[ProductId] = [t.Product].[Id]
INNER JOIN(
SELECT DISTINCT [t4].*
FROM (
    SELECT TOP(1) [t3].[Id]
    FROM[Tickets] AS[t3]
    LEFT JOIN[Abonents] AS[t.Abonent1] ON[t3].[AbonentId] = [t.Abonent1].[Id]
    INNER JOIN[Statuses] AS[t.Status1] ON[t3].[StatusId] = [t.Status1].[Id]
    LEFT JOIN[AspNetUsers] AS[t.AppUser1] ON[t3].[AppUserId] = [t.AppUser1].[Id]
    LEFT JOIN[Groups] AS[t.Group1] ON[t3].[GroupId] = [t.Group1].[Id]
    WHERE[t3].[Id] = @__id_0
    ORDER BY[t3].[Id]
) AS[t4]
) AS[t5] ON[t.TicketProducts].[TicketId] = [t5].[Id]
ORDER BY[t5].[Id]
SELECT[t.Events].[Id], ...
FROM[TicketEvents] AS[t.Events]
LEFT JOIN[AspNetUsers] AS[t.User] ON[t.Events].[AppUserId] = [t.User].[Id]
LEFT JOIN[Messages] AS[t.Message] ON[t.Events].[MessageId] = [t.Message].[Id]
LEFT JOIN[Contacts] AS[t.Message.Contact] ON[t.Message].[ContactId] = [t.Message.Contact].[Id]
INNER JOIN(
SELECT DISTINCT [t7].*
FROM (
    SELECT TOP(1) [t6].[Id]
    FROM[Tickets] AS[t6]
    LEFT JOIN[Abonents] AS[t.Abonent2] ON[t6].[AbonentId] = [t.Abonent2].[Id]
    INNER JOIN[Statuses] AS[t.Status2] ON[t6].[StatusId] = [t.Status2].[Id]
    LEFT JOIN[AspNetUsers] AS[t.AppUser2] ON[t6].[AppUserId] = [t.AppUser2].[Id]
    LEFT JOIN[Groups] AS[t.Group2] ON[t6].[GroupId] = [t.Group2].[Id]
    WHERE[t6].[Id] = @__id_0
    ORDER BY[t6].[Id]
) AS[t7]
) AS[t8] ON[t.Events].[TicketId] = [t8].[Id]
ORDER BY[t8].[Id], [t.Message].[Id]
SELECT[t.Message.Attachments].[Id], ...
FROM[Attachments] AS[t.Message.Attachments]
INNER JOIN(
SELECT DISTINCT [t.Message0].[Id], [t11].[Id] AS [Id0]
FROM[TicketEvents] AS [t.Events0]
LEFT JOIN [AspNetUsers] AS[t.User0] ON [t.Events0].[AppUserId] = [t.User0].[Id]
LEFT JOIN [Messages] AS[t.Message0] ON [t.Events0].[MessageId] = [t.Message0].[Id]
LEFT JOIN [Contacts] AS[t.Message.Contact0] ON [t.Message0].[ContactId] = [t.Message.Contact0].[Id]
INNER JOIN (
    SELECT DISTINCT [t10].*
    FROM (
        SELECT TOP(1) [t9].[Id]
        FROM[Tickets] AS[t9]
        LEFT JOIN[Abonents] AS[t.Abonent3] ON[t9].[AbonentId] = [t.Abonent3].[Id]
        INNER JOIN[Statuses] AS[t.Status3] ON[t9].[StatusId] = [t.Status3].[Id]
        LEFT JOIN[AspNetUsers] AS[t.AppUser3] ON[t9].[AppUserId] = [t.AppUser3].[Id]
        LEFT JOIN[Groups] AS[t.Group3] ON[t9].[GroupId] = [t.Group3].[Id]
        WHERE[t9].[Id] = @__id_0
        ORDER BY[t9].[Id]
    ) AS[t10]
) AS[t11] ON[t.Events0].[TicketId] = [t11].[Id]
) AS[t12] ON[t.Message.Attachments].[MessageId] = [t12].[Id]
ORDER BY[t12].[Id0], [t12].[Id]
Answer 1

Всё наоборот: .Include() требуются, чтобы уменьшить число запросов.

В классическом EF дело обстояло так. Если вы сначала получите список Tickets, потом попросите у БД ticket.TicketContacts и т.д., то выполнится два запроса: SELECT * FROM Tickets и SELECT * FROM TicketContacts WHERE TICKET_ID = @ticketId (условно)

Если же вызовете Tickets.Include(t=>t.TicketContacts), то выполнится единственный запрос SELECT T.*, G.* FROM Tickets T LEFT JOIN TicketContacts G ON G.TICKET_ID = T.ID (опять же, условно. скорее всего будут перечислены поля и вообще)

В EF.Core, если мне не изменяет память, возможен только второй вариант: автоматическая ленивая загрузка не реализована.

Так что вы поступаете абсолютно правильно. Объединение таблиц - то, для чего созданы серверы реляционных БД, пусть они этим и занимаются.

UPD: В документации MS написано, что объединение в единственный запрос - фича EF Core 3, а в версии 2 могло отправиться несколько запросов. По той же ссылке рекомендован пакет, который включает ленивую загрузку в EF Core (но тогда точно запросов будет много).

Answer 2

Если у вас крупное реальное enterprise приложение, то да, иногда приходится писать весьма сложную логику запросов и не всегда даже linq'шных, а приходится писать крупные полотна чистого SQL.

Я не уверен, но мне кажется что вы не совсем правы насчёт того, что будут выполняться отдельные запросы -- потому что насколько я читал про core (сам не часто сталкивался на практике) иногда может делать JOIN а иногда и отдельный запрос. А вот EF6 как раз нагородит огромный, сложный но точно один запрос.

Варианты упрощения тут особо не просматриваются -- у всё include и then include.

Максимум -- можете попробовать перейти на ОРМ Dapper, что даст вам возможность писать raw sql запросы в режиме multiple results, иногда это оказывается полезнее, чем тащить громоздкий linq.

Я пробовал скрестить dapper с EF, вполне получается. Примеры с multiple results можете поискать не только в документации, но и на so бывает, вот например: Как получить связанные сущности на даппере?

Answer 3

Эванс в книге «Предметно-ориентированное проектирование» рекомендует опираться на понятие агрегата. Предполагается, что сущность-агрегат может содержать другие сущности, которые без неё не имеют смысла и не могут быть доступны.

Классический пример — сущность Заказ с вложенными Позициями заказа.

В то же время, если сущность должна быть доступна отдельно в каких-то сценариях, то она не может быть вложенной. Это отдельный агрегат.

Возможно, у вас эта ситуация. Возможно, сообщения (message) и пользователи (user) могут быть доступны вне контекста тикета (ticket).

В целом, несколько Include это вполне нормально, но у вас их как-то и правда много.

READ ALSO
Нужно ввести значение переменной с клавиатуры в классе и в основном коде эту переменную добавить в List<>

Нужно ввести значение переменной с клавиатуры в классе и в основном коде эту переменную добавить в List<>

У меня есть 4 фигуры (соответственно 4 класса) и нужно каждой фигуре ввести свои значения, чтобы подсчитать площадьНужно реализовать ввод...

295
Не находится файл на локальном сервере

Не находится файл на локальном сервере

Всё происходит на Денвере Файл находящийся в папке css при css коде: 'background: url(/images/sprite

94
Задать глобальный стиль для popup/tooltip/title

Задать глобальный стиль для popup/tooltip/title

Есть простой <span title="заполните поле">*</span>

168