高效理解 FreeSql WhereDynamicFilter,深入了解設計初衷[.NET ORM]

💻 前言

最近幾乎每天40度,越熱越不想面對電腦,還好開源項目都比較穩定沒那麼多待解決問題,趁着暑假帶着女兒學習游泳已略有小成。游泳好處太多了,建議有孩子的都去學學,我是在岸邊指導大約一周左右就學會了,目前可游200米。

FreeSql 有一個用戶很迷的功能 WhereDynamicFilter 動態表格查詢,本文講解它的設計初衷,如何高效理解,從此不再迷惑。

小時候學習編程,老師經常教導我們,程序 = 數據結構 + 算法,今天就以我自身的認知講解該功能的完整設計過程,其中包含數據結構和算法。

自從編寫了.NET ORM 開源項目,每次寫文章必有黑子,上次發表的文章評論:

《[開源精品] .NET Redis Client 又多了一個選擇》

@China-Mr-zhong說:寫的項目 都很爛 怎麼好意思 說是精品

@FreeSql回復:

黑子,快回去先學下做人,黑完一次又一次,你代碼寫得真好。

原來他也是 ORM 作者,開源作品:Fast.Framework

而我上次發表的文章並非 ORM 啊,面對這樣的社區圈子,有時候真的無語,這段被黑的內容是事後編輯的,因為現在這篇文章已經有黑子開始點反對了。


🌳 ORM概念

對象關係映射(Object Relational Mapping,簡稱ORM)模式是一種為了解決面向對象與關係數據庫存在的互不匹配的現象的技術。簡單的說,ORM是通過使用描述對象和數據庫之間映射的元數據,將程序中的對象自動持久化到關係數據庫中。

FreeSql 是 .Net ORM,能支持 .NetFramework4.0+、.NetCore、Xamarin、MAUI、Blazor、以及還有說不出來的運行平台,因為代碼綠色無依賴,支持新平台非常簡單。目前單元測試數量:8500+,Nuget下載數量:900K+。QQ群:4336577(已滿)、8578575(在線)、52508226(在線)

FreeSql 使用最寬鬆的開源協議 MIT //github.com/dotnetcore/FreeSql ,完全可以商用,文檔齊全,甚至拿去賣錢也可以。

FreeSql 主要優勢在於易用性上,基本是開箱即用,在不同數據庫之間切換兼容性比較好,整體的功能特性如下:

  • 支持 CodeFirst 對比結構變化遷移;
  • 支持 DbFirst 從數據庫導入實體類;
  • 支持 豐富的表達式函數,自定義解析;
  • 支持 批量添加、批量更新、BulkCopy;
  • 支持 導航屬性,貪婪加載、延時加載、級聯保存;
  • 支持 讀寫分離、分表分庫,租戶設計;
  • 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/達夢/神通/人大金倉/翰高/MsAccess Ado.net 實現包,以及 Odbc 的專門實現包;

8500+個單元測試作為基調,支持10多數數據庫,我們提供了通用Odbc理論上支持所有數據庫,目前已知有群友使用 FreeSql 操作華為高斯、mycat、tidb 等數據庫。安裝時只需要選擇對應的數據庫實現包:

dotnet add packages FreeSql.Provider.MySql


🎣 需求矛盾

雖然 ORM 有理論定義支撐,但實際開發過程中,難免遇到動態查詢的需求,常見的有後台管理系統用戶自定義過濾查詢,如:

鑒於實際與理論的矛盾,導致很多非常實用的功能類庫讓一些人詬病,指這是 SqlHelper,並非 ORM,在此不便理論,功過自在人心。


📰 數據結構

數據結構的定義,決定了功能的使用深度,這塊也參考了一些竟品類似的功能,實際在 .NET ORM 領域很少有完美並簡單的現實,要麼使用太複雜,要麼不支持深層級。

類似的功能其實市面產品應用挺廣泛,幾乎已經形成了一套成熟的產品規則。如果不是親身經歷過類似產品,是很難定義出完美的數據結構的,作為一個公眾開源項目,API 一旦確定再改是非常痛苦的決定,用戶升級不兼容的情況不僅會影響 FreeSql 口碑,還會讓使用者進退兩難,到底要不要升級?好在 FreeSql 從 2018 年最初理念保持至今,關於前後破壞性升級幾乎沒有。

最終根據對 SQL 邏輯表達式的理解,加上參考 JAVA 一個知名的後台開源框架,取長補短確定了最終數據結構。

說這麼多無外乎三個重點:

1、自己不熟悉的,多方面學習,接納更成熟的方案;

2、自己要是沒想好怎麼做,多觀察再做;

3、多思考用戶場景;

我們需要考慮的場景有以下幾種:

1、WHERE id = 1

{
    "Field": "id",
    "Operator": "Equals",
    "Value": 1
}

2、WHERE id = 1 AND id = 2

{
    "Logic": "And",
    "Filters":
    [
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 1
        },
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 2
        }
    ]
}

3、WHERE id IN (1,2)

{
    "Field": "id",
    "Operator": "Contains",
    "Value": [1,2] //或者 "1,2"
}

4、WHERE id = 1 OR id = 2

{
    "Logic": "Or",
    "Filters":
    [
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 1
        },
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 2
        }
    ]
}

5、WHERE id = 1 AND (id = 2 OR id = 3)

注意優先級,它不是 id = 1 AND id = 2 OR id = 3

{
    "Logic": "And",
    "Filters":
    [
        {
            "Field": "id",
            "Operator": "Equals",
            "Value": 1
        },
        {
            "Logic": "Or",
            "Filters":
            [
                {
                    "Field": "id",
                    "Operator": "Equals",
                    "Value": 2
                },
                {
                    "Field": "id",
                    "Operator": "Equals",
                    "Value": 3
                }
            ]
        }
    ]
}

第5個例子最特別,這也是為什麼 WhereDynamicFilter 數據結構定義成樹型的主要原因。

關於 Operator 我們需要以下使用場景:

  • Contains/StartsWith/EndsWith/NotContains/NotStartsWith/NotEndsWith:包含/不包含,like ‘%xx%’,或者 like ‘xx%’,或者 like ‘%xx’
  • Equal/NotEqual:等於/不等於
  • GreaterThan/GreaterThanOrEqual:大於/大於等於
  • LessThan/LessThanOrEqual:小於/小於等於
  • Range:範圍查詢
  • DateRange:日期範圍,有特殊處理 value[1] + 1
  • Any/NotAny:是否符合 value 中任何一項(直白的說是 SQL IN)
  • Custom:自定義解析

最終完整的 c# 數據結構類定義如下:

/// <summary>
/// 動態過濾條件
/// </summary>
[Serializable]
public class DynamicFilterInfo
{
    /// <summary>
    /// 屬性名:Name
    /// 導航屬性:Parent.Name
    /// 多表:b.Name
    /// </summary>
    public string Field { get; set; }
    /// <summary>
    /// 操作符
    /// </summary>
    public DynamicFilterOperator Operator { get; set; }
    /// <summary>
    /// 值
    /// </summary>
    public object Value { get; set; }

    /// <summary>
    /// Filters 下的邏輯運算符
    /// </summary>
    public DynamicFilterLogic Logic { get; set; }
    /// <summary>
    /// 子過濾條件,它與當前的邏輯關係是 And
    /// 注意:當前 Field 可以留空
    /// </summary>
    public List<DynamicFilterInfo> Filters { get; set; }
}

public enum DynamicFilterLogic { And, Or }
public enum DynamicFilterOperator
{
    /// <summary>
    /// like
    /// </summary>
    Contains,
    StartsWith,
    EndsWith,
    NotContains,
    NotStartsWith,
    NotEndsWith,

    /// <summary>
    /// =
    /// Equal/Equals/Eq 效果相同
    /// </summary>
    Equal,
    /// <summary>
    /// =
    /// Equal/Equals/Eq 效果相同
    /// </summary>
    Equals,
    /// <summary>
    /// =
    /// Equal/Equals/Eq 效果相同
    /// </summary>
    Eq,
    /// <summary>
    /// <>
    /// </summary>
    NotEqual,

    /// <summary>
    /// >
    /// </summary>
    GreaterThan,
    /// <summary>
    /// >=
    /// </summary>
    GreaterThanOrEqual,
    /// <summary>
    /// <
    /// </summary>
    LessThan,
    /// <summary>
    /// <=
    /// </summary>
    LessThanOrEqual,

    /// <summary>
    /// >= and <
    /// 此時 Value 的值格式為逗號分割:value1,value2 或者數組
    /// </summary>
    Range,

    /// <summary>
    /// >= and <
    /// 此時 Value 的值格式為逗號分割:date1,date2 或者數組
    /// 這是專門為日期範圍查詢定製的操作符,它會處理 date2 + 1,比如:
    /// 當 date2 選擇的是 2020-05-30,那查詢的時候是 < 2020-05-31
    /// 當 date2 選擇的是 2020-05,那查詢的時候是 < 2020-06
    /// 當 date2 選擇的是 2020,那查詢的時候是 < 2021
    /// 當 date2 選擇的是 2020-05-30 12,那查詢的時候是 < 2020-05-30 13
    /// 當 date2 選擇的是 2020-05-30 12:30,那查詢的時候是 < 2020-05-30 12:31
    /// 並且 date2 只支持以上 5 種格式 (date1 沒有限制)
    /// </summary>
    DateRange,

    /// <summary>
    /// in (1,2,3)
    /// 此時 Value 的值格式為逗號分割:value1,value2,value3... 或者數組
    /// </summary>
    Any,
    /// <summary>
    /// not in (1,2,3)
    /// 此時 Value 的值格式為逗號分割:value1,value2,value3... 或者數組
    /// </summary>
    NotAny,

    /// <summary>
    /// 自定義解析,此時 Field 為反射信息,Value 為靜態方法的參數(string)
    /// 示範:{ Operator: "Custom", Field: "RawSql webapp1.DynamicFilterCustom,webapp1", Value: "(id,name) in ((1,'k'),(2,'m'))" }
    /// 注意:使用者自己承擔【注入風險】
    /// 靜態方法定義示範:
    /// namespace webapp1
    /// {
    /// public class DynamicFilterCustom
    /// {
    /// [DynamicFilterCustom]
    /// public static string RawSql(object sender, string value) => value;
    /// }
    /// }
    /// </summary>
    Custom
}

/// <summary>
/// 授權 DynamicFilter 支持 Custom 自定義解析
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class DynamicFilterCustomAttribute : Attribute { }

📡 安全考慮

由於 ISelect.WhereDynamicFilter 方法實現動態過濾條件(與前端交互),在 SQL 注入安全防禦這塊一定要進行到底,主要思考如下:

1、Field 只允許傳遞 c# 實體屬性名(不支持使用數據庫字段名,甚至直接使用 SQL 內容片段);

2、Operator 只允許規定的枚舉操作類型;

3、Value 必須根據 Operator 進行強制類型檢查,比如 “1,2” + Any 檢索出來的數據是 int[] { 1,2 };

4、Operator Custom 類型支持用戶自行擴展,可現實更自由的查詢;


⚡ 算法

如果把數據結構定義成靈魂,那算法就是驅殼,實現 WhereDynamicFilter 的核心算法是遞歸樹結構。

感興趣的朋友可以直接去源碼查看實現://github.com/dotnetcore/FreeSql


🌌 難理解

WhereDynamicFilter 功能2020年上線到現在,我個人都覺得其實蠻難理解的,更不要提很多使用者反饋。主要原因是數據結構為樹結構,通常80%的人只是簡單的一層 AND/OR 需求,他們很少會遇到深層級的自定義查詢。

但是作為功能性 ORM 類庫,應該滿足更多適用範圍,而不是妥協為求簡單來實現功能。

其實便於理解也不難,只要掌握以下方法:

1、Logic 是設置 Filters 數組下的邏輯關係(這很重要,一定要理解正確)

為了解決 WHERE id = 1 AND (id = 2 OR id = 3) 優先級問題,Filters 更像一對括號

{
    "Logic": "And",
    "Filters":
    [
        { "Field": "id", "Operator": "Equals", "Value": 1 },
        {
            "Logic": "Or",
            "Filters":
            [
                { "Field": "id", "Operator": "Equals", "Value": 2 },
                { "Field": "id", "Operator": "Equals", "Value": 3 }
            ]
        }
    ]
}

2、Field/Operator/Value 與 Logic/Filters 不要同時設置(避免理解困難)

3、刪除 JSON 中不必要的內容

這個病不好治,因為強類型對象產生的默認 json 內容,即使無用的屬性也序列化了。

{
    "Field": null,
    "Operator": "And",
    "Value": null,
    "Logic": "Or",
    "Filters":
    [
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-01",
            "Logic": "And",
            "Fitlers": null
        },
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-02",
            "Logic": "And",
            "Fitlers": null
        }
    ]
}

以上類型改成如下,是不是更好理解?

{
    "Logic": "Or",
    "Filters":
    [
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-01"
        },
        {
            "Field": "Name-1",
            "Operator": "Equals",
            "Value": "ye-02"
        }
    ]
}

🚀 最終功能

一個任意定製的高級查詢功能預覽如下:

前端只需要按要求組裝好 DynamicFilterInfo 對應的 JSON 數據內容,後台就可輕易完成高級過濾查詢,有多輕易呢?

var dyfilter = JsonConvert.DeserializeObject<DynamicFilterInfo>(jsonText);
var list = fsql.Select<T>().WhereDynamicFilter(dyfilter).ToList();

⛳ 結束語

希望這篇文章能幫助大家從 WhereDynamicFilter 的設計初衷,輕鬆理解並熟練掌握它,為企業的項目研發貢獻力量。

開源地址://github.com/dotnetcore/FreeSql


作者是什麼人?

作者是一個入行 18年的老批,他目前寫的.net 開源項目有:

開源項目 描述 開源地址 開源協議
ImCore 架構最簡單,擴展性最強的聊天系統架構 //github.com/2881099/im 最寬鬆的 MIT 協議,可商用
FreeRedis 最簡單的 RediscClient //github.com/2881099/FreeRedis 最寬鬆的 MIT 協議,可商用
csredis //github.com/2881099/csredis 最寬鬆的 MIT 協議,可商用
FightLandlord 鬥地主單機或網絡版 //github.com/2881099/FightLandlord 最寬鬆的 MIT 協議,學習用途
IdleScheduler 定時任務 //github.com/2881099/IdleBus/tree/master/IdleScheduler 最寬鬆的 MIT 協議,可商用
IdleBus 空閑容器 //github.com/2881099/IdleBus 最寬鬆的 MIT 協議,可商用
FreeSql 國產最好用的 ORM //github.com/dotnetcore/FreeSql 最寬鬆的 MIT 協議,可商用
FreeSql.Cloud 分佈式事務tcc/saga //github.com/2881099/FreeSql.Cloud 最寬鬆的 MIT 協議,可商用
FreeSql.AdminLTE 低代碼後台管理項目生成 //github.com/2881099/FreeSql.AdminLTE 最寬鬆的 MIT 協議,可商用
FreeSql.DynamicProxy 動態代理 //github.com/2881099/FreeSql.DynamicProxy 最寬鬆的 MIT 協議,學習用途

需要的請拿走,這些都是最近幾年的開源作品,以前更早寫的就不發了。

QQ群:4336577(已滿)、8578575(在線)、52508226(在線)