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

FreeSql 發表於 2022-07-17
.Net SQL

💻 前言

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

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

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

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

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

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

@FreeSql回覆:

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

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

原來他也是 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 https://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 有理論定義支撐,但實際開發過程中,難免遇到動態查詢的需求,常見的有後臺管理系統使用者自定義過濾查詢,如:

高效理解 FreeSql WhereDynamicFilter,深入瞭解設計初衷[.NET 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 的核心演算法是遞迴樹結構。

感興趣的朋友可以直接去原始碼檢視實現:https://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"
        }
    ]
}

🚀 最終功能

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

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

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

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

⛳ 結束語

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

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


作者是什麼人?

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

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

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

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