EF Core For MySql查詢中使用DateTime.Now做爲查詢條件的一個小問題

背景

最近一直忙於手上澳洲線上項目的總體遷移和升級的準備工做,致使博客和公衆號停更。本週終於艱難的完成了任務,藉此機會,總結一下項目中遇到的一些問題。sql

EF Core一直是咱們團隊中中小型項目經常使用的ORM框架,在使用SQL Server做爲持久化倉儲的場景一下,一直表現還中規中矩。可是在本次項目中,項目使用了MySql做爲持久化倉儲。爲了與EF Core集成,團隊使用了Pomelo.EntityFrameworkCore.MySql做爲EF Core For MySql的擴展。在開發過程當中,團隊遇到了各類各樣在SQL Server場景下沒有遇到過的問題,其中最奇怪的,也是隱藏最深的問題,就是將DateTime.Now做爲查詢條件,產生了非預期的結果。數據庫

問題場景

本週在項目升級的過程當中,客戶反饋了一個問題。c#

在當前系統的Dashboard頁面,有一個消息提醒功能,客戶能夠自定義一些消息,而且指定提醒的日期。客戶遇到的問題是一般添加的消息提醒,在指定日期的上午時間段是不會顯示,只有在下午時間段才能看到,好比說客戶指定2019年10月26號看到一個的消息提醒,可是在10月26日這天早上8:00-12:00這個時間段,系統老是看不到提醒,只有到了下午的時間段才能看到提醒。框架

PS:這裏客戶表達的只是個籠統的問題,但問題確實是上午的大部分時間是看不到消息提醒的,但並非精確到中午12:00點這個時間, 因此此處沒必要過於糾結於具體的時間。ide

查看問題代碼

看到這個問題的時候,我本身也很奇怪,難道代碼或者數據庫使用了時區,致使查詢出現了誤差?函數

因而我就Review了一下此處的查詢, 代碼以下。工具

var query = DbContext.CRM_Note_Reminders
    .Include(x => x.CRM_Note)
    .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= DateTime.Now.Date)
     .ToList();

PS: 這裏可能有同窗會有疑問,爲啥不用DbFunctions.DiffDays? 緣由是DbFunctions.DiffDays是 EF Core for SQLServer的擴展方法,針對MySql尚未官方的實現方案。翻譯

從這個查詢中,我沒有看出任何問題,因而我直接藉助一些日誌工具,將EF Core生成的查詢語句的輸出了出來。日誌

其中WHERE條件部分以下:code

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE))
AND (CONVERT(`x`.`Reminder_Date`, date) 
  <= CONVERT(CURRENT_TIMESTAMP(), date)))

這裏CURRENT_TIMESTAMP()是MySql的內置函數,與SQLServer的內置函數GETDATE()不一樣,CURRENT_TIMESTAMP()默認返回的是UTC時間。所以咱們大概能知道,爲何澳洲客戶會遇到上面的場景了。

因爲澳洲處於東10區,與UTC時間有+10個小時的時差,因此當澳洲上午的10點以前,UTC時間都是在當前澳洲日期的前一天,因此係統中出現了當天的消息提醒在上午時間段不能正常顯示的問題。

PS: 因爲澳洲是分冬令時和夏令時的,夏令時時間要加一個小時,因此實際上客戶在天天的11點以前都沒法看到正確的消息提醒。

深刻思考

你這可能會很是奇怪,爲何DateTime.Now會被轉化成內置函數CURRENT_TIMESTAMP(),而沒有使用咱們傳入的值DateTime.Now.Date呢?

其實EF/EF Core在查詢是時候是分2個階段的,一個是組合查詢表達式樹的階段,一個是真正的查詢階段。

在組合查詢表達式樹的階段,EF/EF Core只會去組合表達式,而不會去嘗試計算表達式的值,因此這個階段DateTime.Now.Date的值並無被計算出來, 在進入正常查詢階段的時候, EF/EF Core會嘗試將查詢表達式樹翻譯成SQL腳本,這時候因爲咱們的EF ProviderMySql Provider, 恰巧DateTime.Now能夠翻譯成Mysql的內置函數CURRENT_TIMESTAMP(), 因此這裏EF/EF Core就跳過了表達式值的計算,直接將其翻譯成了對應的內置函數,因此致使生成的SQL查詢和咱們的預期有誤差。

那麼咱們該如何解決這個問題呢?

解決方案

通過了以上的思考,其實解決這個問題也就很簡單了,咱們能夠將DateTime.Now.Date先計算出來,保存在一個變量中,而後將這個變量傳入查詢中。

var today = DateTime.Now.Date;

var query = DbContext.CRM_Note_Reminders
     .Include(x => x.CRM_Note)
     .Where(x => !x.CRM_Note.Is_Deleted 
             && !x.Is_Deleted
             && x.Reminder_Date.Date <= today)
     .ToList();

由今生成的MySQL腳本以下:

WHERE (((`x.CRM_Note`.`Is_Deleted` = FALSE) 
AND (`x`.`Is_Deleted` = FALSE)) 
AND (CONVERT(`x`.`Reminder_Date`, date) <= @__date_0))

這樣咱們就獲得了一個正確的結果,澳洲客戶也就收到了正確的消息。

是否是有種差之毫釐,謬以千里的感受呢?

相關文章
相關標籤/搜索