簡體   English   中英

如何進一步過濾查詢的.Include(.Where()) 並將其反映在 Include() 之外

[英]How to further filter on a query's .Include(.Where()) and have it reflected outside of the Include()

我知道我的問題有點令人困惑,因為我還沒有找到更好的提問方式,但我確信這不是一個很難解決的問題。

這是正在發生的事情:

我應該在實體存儲庫中名為GetAllPlaces的方法中返回一個List<Place>

放置實體:

public Guid PlaceId { get; set; }
public string Name { get; set; }
public List<Hour> Hours { get; set; }

小時實體:

public Guid HourId { get; set; }
public Guid DayOfTheWeekId { get; set; }
public DayOfTheWeek DayOfTheWeek { get; set; }
public DateTime OpenHour { get; set; }
public DateTime CloseHour { get; set; }
public Guid PlaceId { get; set; }
public Place Place { get; set; }

每個Place都有一個List<Hour>屬性。 我試圖過濾這個小時列表,以不返回對該方法的調用者關閉的地方。 到目前為止我所擁有的是我正在過濾以僅在該地點的時區中包含該地點的今天時間:

public async Task<IReadOnlyList<Place>>
            GetAllPlacesAsync()
        {    
            var places = await context.Places
                .AsNoTracking()

                .Include(s => s.Hours
                    // here I'm filtering to get just today's Hour like explained previously
                    .Where(d => d.DayOfTheWeek.DayName
                        == TimeZoneInfo
                        .ConvertTime(DateTime.Now,
                            TimeZoneInfo
                            .FindSystemTimeZoneById(d.Place.Timezone.Name))
                            .DayOfWeek
                            .ToString()).FirstOrDefault())
                // a second .Where() would filter on the .Include()
                // or on the "places" List but not on its Hours.
                // How to further filter to do something like this:
                // if Place.Hour.Open <= timeNowInPlaceTimezone
                // && Place.Hour.Close >= timeNowInPlaceTimezone ? passToList : dontPassToList

                .Distinct()
                .ToListAsync();

            return places;
        }

您知道我如何過濾它以僅獲取該地點的Hour在其時區今天的開放時間和結束時間之間的地方嗎?

編輯

似乎有效的是您所有答案的混合:

var places = await context.Places
                .AsNoTracking()

                // to let the mobile app display if the Place
                // is open or closed depending on its Timezone.
                .Where(place => place.StoreHours
                    .Any(storeHour =>
                    // check if the day is the same in its timezone
                    storeHour.DayOfTheWeek.DayName == TimeZoneInfo
                            .ConvertTime(DateTime.Now,
                                TimeZoneInfo
                                .FindSystemTimeZoneById(storeHour.Place.Timezone.Name))
                                .DayOfWeek
                                .ToString()
                    // check if the open hour is between open and closed in its timezone
                    && storeHour.OpenHour <= TimeZoneInfo
                            .ConvertTime(DateTime.Now,
                                TimeZoneInfo
                                .FindSystemTimeZoneById(storeHour.Place.Timezone.Name))
                    // check if the close hour is between open and closed in its timezone
                    && storeHour.CloseHour >= TimeZoneInfo
                            .ConvertTime(DateTime.Now,
                                TimeZoneInfo
                                .FindSystemTimeZoneById(storeHour.Place.Timezone.Name))
                        )
                )

                .Distinct()
                .ToListAsync();

            return places;

我找不到創建變量的方法,無論是在.Where()中,就像@TN 說的那樣,使用return關鍵字給我一個異常“ a lambda expression with a statement body cannot be converted to an expression tree ”,我不能'在.Where()之前創建dayOfTheWeek變量,就像@SvyatoslavDanyliv 所說的那樣,因為我需要瀏覽相關數據以訪問數據並針對每個地點進行過濾。

我還沒有測試過,但至少我沒有像以前那樣收到 IDE 的錯誤。

但是,在上面的那個和這個之間哪個更有效?

List<Place> openPlaces = new List<Place>();

            var places = await context.Places
                .AsNoTracking()
                .Distinct()
                .ToListAsync();

            foreach (var place in places)
            {
                if (place.StoreHours.Any(
                    storeHour =>
                        // check if the day is the same in its timezone
                        storeHour.DayOfTheWeek.DayName == TimeZoneInfo
                                .ConvertTime(DateTime.Now,
                                    TimeZoneInfo
                                    .FindSystemTimeZoneById(storeHour.Place.Timezone.Name))
                                    .DayOfWeek
                                    .ToString()
                        // check if the open hour is between open and closed in its timezone
                        && storeHour.OpenHour <= TimeZoneInfo
                                .ConvertTime(DateTime.Now,
                                    TimeZoneInfo
                                    .FindSystemTimeZoneById(storeHour.Place.Timezone.Name))
                        // check if the close hour is between open and closed in its timezone
                        && storeHour.CloseHour >= TimeZoneInfo
                                .ConvertTime(DateTime.Now,
                                    TimeZoneInfo
                                    .FindSystemTimeZoneById(storeHour.Place.Timezone.Name))
                                )
                    )
                {
                    place.IsOpen = true;
                    openPlaces.Add(place);
                }
            }

            // to update the IsOpen property for each Place in the DB with one call
            context.SaveChanges();

            //return places;
            return openPlaces;

首先只是為了澄清我的理解或您的數據:每個地方都有一個List<Hour>集合,其中每個Hour object 都有屬性DayOfTheWeekOpenClose - 類似於 商店營業時間標志上的行項目。 此外,每個place都有一個關聯的時區,在檢查place.Hours集合之前,需要將當前 (UTC) 日期/時間轉換為該時區。

我不相信.Include()是您在這里想要的,因為 function 用於選擇額外的數據以急切地加載結果而不是過濾結果。

我的期望是你想要類似.Where(place => place.Hours.Any(hour => hours-matches-local-time)東西。

但是, hours-matches-local-time部分可能包含復雜的計算,這些計算可能會在.Any()內部針對同一個地方多次進行不必要的評估。 為了避免冗余計算,可以將外層.Where()中的條件做成一個代碼塊,每個地方計算一次本地星期幾和本地時間,賦值給變量,然后在內層重復引用.Any()

嘗試類似的東西:

var places = await context.Places
    .AsNoTracking()
    .Where(place => {
        var localDateTime = ...;
        var dayOfWeekInPlaceTimezone = ...;
        var timeNowInPlaceTimezone  = ...;
        bool isOpen = place.Hours
            .Any(hour =>
                hour.DayOfTheWeek == dayOfWeekInPlaceTimezone
                && hour.Open <= timeNowInPlaceTimezone
                && hour.Close > timeNowInPlaceTimezone
            );
        return isOpen;
    })
    //(not needed) .Distinct() 
    .ToListAsync();

上面使用了 LINQ 方法語法 另一種方法是使用LINQ 查詢語法,它也可以使用let子句計算局部變量。 這篇文章的答案展示了其中一些技術。 (也許在 LINQ 查詢語法方面比我更有經驗的人可以發布翻譯。)

可能還有其他改進機會,例如按時區對地點進行分組,並為每組計算一次本地日期/時間。 .SelectMany()最終將用於處理每個組並拉平結果。 結果可能類似於:

var places = await context.Places
    .AsNoTracking()
    .GroupBy(place => place.Timezone)
    .SelectMany(tzGroup => {
        var timezone = tzGroup.Key;
        var localDateTime = ...;
        var dayOfWeekInPlaceTimezone = ...;
        var timeNowInPlaceTimezone = ...;
        return tzGroup.Where(place =>
            place.Hours.Any(hour =>
                hour.DayOfTheWeek == dayOfWeekInPlaceTimezone
                && hour.Open <= timeNowInPlaceTimezone
                && hour.Close > timeNowInPlaceTimezone
            )
        );
    })
    //(not needed) .Distinct() 
    .ToListAsync();

不確定您關於開/關時間的情況,但查詢應如下所示:

public async Task<IReadOnlyList<Place>>  GetAllPlacesAsync()
{    
    var dayOfWeek = TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo .FindSystemTimeZoneById(d.Place.Timezone.Name))
        .DayOfWeek
        .ToString();

    var places = await context.Places
        .AsNoTracking()
        .Where(p => p.Hours.Any(h =>
                h.DayOfTheWeek.DayName == dayOfWeek 
                && h.Open <= timeNowInPlaceTimezone
                && h.Close >= timeNowInPlaceTimezone
            )
        )
        .ToListAsync();

    return places;
}

為了提高效率,最好使用本機 SQL 在數據庫端執行所有過濾以轉換日期/時間並應用打開/關閉條件。 這將避免在每次執行時將整個 Place 表和可能整個 StoreHours 表加載到 C# 環境中。

存儲過程看起來像:

CREATE PROCEDURE GetOpenPlaces
AS
    SELECT P.*
    FROM Place P
    JOIN Timezone TZ ON TZ.TimezoneId = P.TimezoneId
    CROSS APPLY (
        -- Intermediate local time calculations
        SELECT
            GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE TZ.Name AS LocalDateTime
    ) LT
    CROSS APPLY (
        -- More intermediate local time calculations
        SELECT
            DATENAME(weekday, LT.LocalDateTime) AS LocalWeekdayName,
            CAST(LT.LocalDateTime AS TIME) AS LocalTime
    ) DT
    JOIN DayOfTheWeek DOW ON DOW.DayName = DT.LocalWeekdayName
    WHERE EXISTS (
        SELECT *
        FROM StoreHour H
        WHERE H.PlaceId = P.PlaceId
        AND H.DayOfTheWeekId = DOW.DayOfTheWeekId
        AND H.OpenHour <= DT.LocalTime
        AND (H.CloseHour > DT.LocalTime OR H.CloseHour = '00:00')
    )

上面支持一種特殊情況,其中關閉時間 '24:00' 存儲為 '00:00',因為時間不支持 '24:00'

請參閱此 db<>fiddle以獲取演示,其中包括各種生成的測試數據和用於說明目的的上述查詢的略微修改版本。

這一切都假定您的時區名稱與 SQL 服務器已知的名稱相匹配。 我相信 SQL 服務器時區信息和 .Net TimeZoneInfo 都基於操作系統注冊表數據,只要定期應用更新,這應該是一個有效的假設。

另外,我建議您至少在StoreHour(PlaceId)上定義一個索引,或者(更好) StoreHour(PlaceId, DayOfTheWeekId, OpenHour, CloseHour)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM