Получить соседние объекты - PullRequest
4 голосов
/ 15 апреля 2020

Я пишу функцию, которая получает соседние (предыдущий и следующий) объекты из базы данных на основе даты. Я выяснил, как вернуть соседей в 2 запросах, но я бы предпочел, если бы я мог вытащить обе сущности одновременно.

public interface IHasDateRange
{
    DateTime StartDate { get; set; }
    DateTime EndDate { get; set; }
}

public static (TEntity Previous, TEntity Next) GetNeighborsOrDefault<TEntity>(
    this IQueryable<TEntity> query, JustDate startDate)
        where TEntity : class, IHasDateRange
{
    var previous = query.Where(x => x.StartDate < startDate)
            .OrderByDescending(x => x.StartDate)
            .FirstOrDefault();

    var next = query.Where(x => x.StartDate > startDate)
        .OrderBy(x => x.StartDate)
        .FirstOrDefault();

    return (previous, next);
}

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

РЕДАКТИРОВАТЬ Я думаю, что есть способ сделать это, если я удалите фильтр Где для даты начала и вместо этого рассчитайте расстояние. Я все еще застрял, но у меня есть ощущение, что что-то должно работать.

var previous = query
    .Select(x => new { 
        Entity = x,  
        Distance = DbFunctions.DiffDays(x.StartDate, startDate)
   })
    .Where(x => x.Distance != 0); 

Примечание: предполагается, что у каждого объекта есть уникальная дата начала.

Есть ли простой способ извлечь предыдущие и последующие объекты в одном запросе?

Ответы [ 2 ]

1 голос
/ 18 апреля 2020

Это то же самое, что получить две сущности от (и включая) первой даты до startDate.

query.Where(e => e.StartDate != startDate
    && e.StartDate >= query.OrderByDescending(e1 => e1.StartDate)
        .Where(e1 => e1.StartDate < startDate).Select(e1 => e1.StartDate).FirstOrDefault())
    .OrderBy(e => e.StartDate)
    .Take(2)

Как видите, вы не можете избежать запуска двух запросов, хотя вторая запрос теперь является подзапросом в одном основном запросе.

В EF6 это генерирует довольно сложный запрос, подобный следующему:

SELECT TOP (2)
    ...
    FROM ( SELECT 
        ...
        FROM  [dbo].[Entity] AS [Extent1]
        INNER JOIN  (SELECT TOP (1) [Project1].[StartDate] AS [StartDate]
            FROM ( SELECT 
                [Extent2].[StartDate] AS [StartDate]
                FROM [dbo].[Entity] AS [Extent2]
                WHERE [Extent2].[StartDate] < @p__linq__1
            )  AS [Project1]
            ORDER BY [Project1].[StartDate] DESC ) AS [Limit1] ON 1 = 1
        WHERE ( NOT (([Extent1].[StartDate] = @p__linq__0) AND ((CASE WHEN ([Extent1].[StartDate] IS NULL) THEN cast(1 as bit) ELSE cast(0 as bit) END) = 0))) AND ([Extent1].[StartDate] >= [Limit1].[StartDate])
    )  AS [Project2]
    ORDER BY [Project2].[StartDate] ASC

Я был приятно удивлен, увидев, как ядро ​​EF 3.1.3 генерирует довольно простой запрос, подобный следующему:

SELECT TOP(@__p_2) ...
FROM [Entity] AS [e]
WHERE (([e].[StartDate] <> @__startDate_0) OR [e].[StartDate] IS NULL) AND ([e].[StartDate] >= (
    SELECT TOP(1) [e0].[StartDate]
    FROM [Entity] AS [e0]
    WHERE [e0].[StartDate] < @__startDate_1
    ORDER BY [e0].[StartDate] DESC))
ORDER BY [e].[StartDate]
1 голос
/ 16 апреля 2020

Как насчет того, чтобы взять до и после и исключить середину?

Я считаю, что это все равно будет генерировать два отдельных SQL запроса - один для получения Count() и один для получения результатов, но если вы не хотите добавить поддержку ROW_NUMBER в EF (вы можете расширить для нее EF Core), я не думаю, что есть лучший способ:

var previousAndNext = query.OrderBy(x => x.StartDate)
        .Skip(query.Where(x => x.StartDate < startDate).Count()-1)
        .Take(3)
        .Where(x => x.StartDate != startDate)
        .Take(2) // if startDate not in DB, just get previous and next
        .ToList();
...