1. 程式人生 > >[開源] .Net ORM FreeSql 1.10.0 穩步向前

[開源] .Net ORM FreeSql 1.10.0 穩步向前

# 寫在開頭 FreeSql 是 .NET 開源生態下的 ORM 輪子,轉眼快兩年了,說真的開源不容易(只有經歷過才明白)。今天帶點乾貨和溼貨給大家,先說下溼貨。 認識我的人,知道 CSRedisCore 是我寫的另外一個開源元件,這個專案是 2016 年從 ctstone/csredis 專案 clone 到自己工作的專案中,修改原始碼經過一年多生產考驗,於 2017 年釋出開源 https://github.com/2881099/csredis ctstone/csredis 專案於 2014 年停止了更新,到我手裡完善的功能如下: - 連線池 - 哨兵高可用 - 叢集 - redis 2.8 以上的版本命令補充,包括 Geo、Stream - 通訊協議 bug 修復 暫時想到的只有這些,之後可能再補充。FreeSql 文章標題為什麼要來說 csredis? 這兩年的時間裡 95% 精力都用在了 FreeSql 上面, 5400+ 單元測試、支援十幾種資料庫適配,渣男辜負了 csredis 這個專案。最近一個多月開源圈子的奇葩事接二連三,居然有人跑去 ctstone/csredis 原作者的 issues 告我的狀,這個告狀的人還是 NOPI 原作者,因為當初他自己不維護 NPOI .NET Core 版本了,社群有好人把 .NET Core 版本測試做好了開源(dotnetcore/NPOI),告狀的人很真心厲害,已經成功把 nuget.org/dotnetcore.npoi 整下架了。 他並沒有得到滿足,之後開始針對整個 NCC 社群成員,包括我。 - 他去了 sqlsugar issues 發表,說要找出 FreeSql 抄襲 sqlsugar 的證據 - 他又去 fur issues 發表聲援,說我黑他 - 他還去 csredis 原作者 issues 釋出內容,企圖告我的狀 並不是人人都像你一樣,強迫要求下游專案“歸檔”、“制裁”,試問 mysql 可以要求 mariadb 歸檔?針對 NCC 組織還是針對我本人?CSRedisCore 並不在 NCC 開源組織下!!! 幾天月前我已經開始了新的 redis .NET 開源元件庫的編寫,完全自主的看你能上哪裡告狀。有了這麼長時間的 csredis 經驗,重新寫一個能避免很多問題,設計也會更好,後面我會花大部分時間做新專案,這便是今天帶來的溼貨,敬請期待發布!~! # 入戲準備 2018 年 12 月份開發 FreeSql 到現在,2200 顆星,500 Issues,200K 包下載量。說明還是有開發者關注和喜愛,只要有人關注,就不會停更不修 BUG 一說。大家有興趣可以看看更新記錄,看看我們的程式碼提交量,5400+ 單元測試不說非常多,個人覺得已經超過很多國產專案。 23個月了,FreeSql 還活著,而且生命力頑強見下圖: ![](https://img2020.cnblogs.com/blog/31407/202010/31407-20201022021555253-1521774408.png) 年底釋出 2.0 版本正在收集需求中(歡迎前去 issues 誠意登記),本文將介紹在過去的幾個月完成的一些有意義的功能介紹。 FreeSql 是 .Net ORM,能支援 .NetFramework4.0+、.NetCore、Xamarin、XAUI、Blazor、以及還有說不出來的執行平臺,因為程式碼綠色無依賴,支援新平臺非常簡單。目前單元測試數量:5400+,Nuget下載數量:200K+,原始碼幾乎每天都有提交。值得高興的是 FreeSql 加入了 ncc 開源社群:[https://github.com/dotnetcore/FreeSql](https://github.com/dotnetcore/FreeSql),加入組織之後社群責任感更大,需要更努力做好品質,為開源社群出一份力。 QQ群:4336577(已滿)、8578575(線上)、52508226(線上) 為什麼要重複造輪子? ![](https://img2020.cnblogs.com/blog/31407/202005/31407-20200525013907903-1470982538.png) > FreeSql 主要優勢在於易用性上,基本是開箱即用,在不同資料庫之間切換相容性比較好。作者花了大量的時間精力在這個專案,肯請您花半小時瞭解下專案,謝謝。 FreeSql 整體的功能特性如下: - 支援 CodeFirst 對比結構變化遷移; - 支援 DbFirst 從資料庫匯入實體類; - 支援 豐富的表示式函式,自定義解析; - 支援 批量新增、批量更新、BulkCopy; - 支援 導航屬性,貪婪載入、延時載入、級聯儲存; - 支援 讀寫分離、分表分庫,租戶設計; - 支援 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/達夢/神通/人大金倉/MsAccess Ado.net 實現包,以及 Odbc 的專門實現包; # 乾貨來了 1.5.0 -> 1.10.0 更新的重要功能如下: 一、增加 Firebird 資料庫實現; 二、增加 人大金倉/神通 資料庫的訪問支援; 三、增加 GlobalFilter.ApplyIf 建立動態過濾器; 四、增加 ISelect.InsertInto 將查詢轉換為 INSERT INTO t1 SELECT ... FROM t2 執行插入; 五、增加 IncludeMany(a => a.Childs).ToList(a => new { a.Childs }) 指定集合屬性返回; 六、增加 $"{a.Code}_{a.Id}" lambda 解析; 七、增加 lambda 表示式樹解析子查詢 ToList + string.Join() 產生 類似 group_concat 的效果; 八、增加 SqlExt 常用開窗函式的自定義表示式解析; 九、增加 ISelect/IInsert/IUpdate/IDelete CommandTimeout 方法設定命令超時; 十、完善 WhereDynamicFilter 動態過濾查詢; 十一、增加 BeginEdit/EndEdit 批量編輯資料的功能; 十二、增加 父子表(樹表)遞迴查詢、刪除功能; ![](https://img2020.cnblogs.com/blog/31407/202005/31407-20200525014307613-207448539.png) FreeSql 使用非常簡單,只需要定義一個 IFreeSql 物件即可: ```csharp static IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.DataType.MySql, connectionString) .UseAutoSyncStructure(true) //自動同步實體結構到資料庫 .Build(); //請務必定義成 Singleton 單例模式 ``` # 增加 Firebird 資料庫實現; 它的體積比前輩Interbase縮小了幾十倍,但功能並無閹割。為了體現Firebird短小精悍的特色,開發小組在增加了超級伺服器版本之後,又增加了嵌入版本,最新版本為2.0。Firebird的嵌入版有如下特色: 1、資料庫檔案與Firebird網路版本完全相容,差別僅在於連線方式不同,可以實現零成本遷移。 2、資料庫檔案僅受作業系統的限制,且支援將一個數據庫分割成不同檔案,突破了作業系統最大檔案的限制,提高了IO吞吐量。 3、完全支援SQL92標準,支援大部分SQL-99標準功能。 4、豐富的開發工具支援,絕大部分基於Interbase的元件,可以直接使用於Firebird。 5、支援事務、儲存過程、觸發器等關係資料庫的所有特性。 6、可自己編寫擴充套件函式(UDF)。 7、firebird其實並不是純粹的嵌入式資料庫,embed版只是其眾多版本中的一個。不過做的也很小,把幾個dll加起來才不到5M,但是它支援絕大部份SQL92與SQL99標準 > 嵌入式,等於無需安裝的本地資料庫,歡迎體驗!~~ ---- # 增加 人大金倉/神通 資料庫的訪問支援 天津神舟通用資料技術有限公司(簡稱“神舟通用公司”),隸屬於中國航天科技集團(CASC)。是國內從事資料庫、大資料解決方案和資料探勘分析產品研發的專業公司。公司獲得了國家核高基科技重大專項重點支援,是核高基專項的牽頭承擔單位。自1993年在航天科技集團開展資料庫研發以來,神通資料庫已歷經27年的發展歷程。公司核心產品主要包括神通關係型資料庫、神通KStore海量資料管理系統、神通商業智慧套件等系列產品研發和市場銷售。基於產品組合,可形成支援交易處理、MPP資料庫叢集、資料分析與處理等解決方案,可滿足多種應用場景需求。產品通過了國家保密局涉密資訊系統、公安部等保四級、軍B +級等安全評測和認證。 北京人大金倉資訊科技股份有限公司(以下簡稱“人大金倉”)是具有自主智慧財產權的國產資料管理軟體與服務提供商。人大金倉由中國人民大學一批最早在國內開展資料庫教學、科研、開發的專家於1999年發起創立,先後承擔了國家“863”、“核高基”等重大專項,研發出了具有國際先進水平的大型通用資料庫產品。2018年,人大金倉申報的“資料庫管理系統核心技術的創新與金倉資料庫產業化”專案榮獲2018年度國家科學技術進步二等獎,產學研的融合進一步助力國家資訊化建設。 隨著華為、中興事務,國產資料庫市場相信是未來是趨勢走向,縱觀 .net core 整個圈子對國產神舟通用、人大金倉資料庫的支援幾乎為 0,今天 FreeSql ORM 可以使用 CodeFirst/DbFirst 兩種模式進行開發。 並且聲稱:FreeSql 對各資料庫沒有親兒子一說,除了 MsAcces 其他全部是親兒子,在功能提供方面一碗水端平。 ![](https://img2020.cnblogs.com/blog/31407/202005/31407-20200525014801483-1494727409.png) 眾所周知 EFCore for oracle 問題多,並且現在才剛剛更新到 3.x,在這樣的背景下,一個國產資料庫更不能指望誰實現好用的 EFCore。目前看來除了 EFCore for sqlserver 我們沒把握完全佔優勢,起碼在其他資料庫肯定是我們更接地氣。 使用 FreeSql 訪問人大金倉/神通 資料庫,只需要修改程式碼如下即可: ```csharp static IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.DataType.ShenTong, connectionString) //修改 DataType 設定切換資料庫 .UseAutoSyncStructure(true) //自動同步實體結構到資料庫 .Build(); //請務必定義成 Singleton 單例模式 ``` ---- # 增加 GlobalFilter.ApplyIf 建立動態過濾器; FreeSql 使用全域性過濾器非常簡單,我們的過濾器支援多表查詢、子查詢,只需要設定一次: ```csharp public static AsyncLocal TenantId { get; set; } = new AsyncLocal(); fsql.GlobalFilter .Apply("name1", a => a.IsDeleted == false) .ApplyIf("tenant", () => TenantId.Value != Guid.Empty, a => a.TenantId == TenantId.Value); ``` 上面增加了兩個過濾器,tenant 第二個引數正是增加的功能,當委託條件成立時才會附加過濾器。 ---- # 增加 ISelect.InsertInto 將查詢轉換為 INSERT INTO t1 SELECT ... FROM t2 執行插入; ```csharp int affrows = fsql.Select() .Limit(10) .InsertInto(null, a => new Topic2 { Title = a.Title }); ``` ```sql INSERT INTO `Topic2`(`Title`, `Clicks`, `CreateTime`) SELECT a.`Title`, 0, '0001-01-01 00:00:00' FROM `Topic` a limit 10 ``` 注意:因為 Clicks、CreateTime 沒有被選擇,所以使用目標實體屬性 [Column(InsertValueSql = xx)] 設定的值,或者使用目標實體屬性的 c# 預設值。 又一次完善了批量操作資料的功能,之前已經有的功能如下: - fsql.InsertOrUpdate 相當於 Merge Into/on duplicate key update | Database | Features | | Database | Features | | -- | -- | -- | -- | -- | | MySql | on duplicate key update | | 達夢 | merge into | | PostgreSQL | on conflict do update | | 人大金倉 | on conflict do update | | SqlServer | merge into | | 神通 | merge into | | Oracle | merge into | | MsAccess | 不支援 | | Sqlite | replace into | | | | | Firebird | merge into | | | | - fsql.Insert(陣列).ExecuteAffrows() 相當於批量插入 ```csharp var t2 = fsql.Insert(items).ExecuteAffrows(); //INSERT INTO `Topic`(`Clicks`, `Title`, `CreateTime`) //VALUES(?Clicks0, ?Title0, ?CreateTime0), (?Clicks1, ?Title1, ?CreateTime1), //(?Clicks2, ?Title2, ?CreateTime2), (?Clicks3, ?Title3, ?CreateTime3), //(?Clicks4, ?Title4, ?CreateTime4), (?Clicks5, ?Title5, ?CreateTime5), //(?Clicks6, ?Title6, ?CreateTime6), (?Clicks7, ?Title7, ?CreateTime7), //(?Clicks8, ?Title8, ?CreateTime8), (?Clicks9, ?Title9, ?CreateTime9) ``` 當插入大批量資料時,內部採用分割分批執行的邏輯進行。分割規則: | | 數量 | 引數量 | | -- | -- | -- | | MySql | 5000 | 3000 | | PostgreSQL | 5000 | 3000 | | SqlServer | 1000 | 2100 | | Oracle | 500 | 999 | | Sqlite | 5000 | 999 | - fsql.Insert(陣列).ExecuteSqlBulkCopy、ExecutePgCopy、ExecuteMySqlBulkCopy - fsql.Update\().SetSource(陣列).ExecuteAffrows() 相當於批量更新 ---- # 增加 IncludeMany(a => a.Childs).ToList(a => new { a.Childs }) 指定集合屬性返回; 這個功能實在太重要了,在此之前 IncludeMany 和 ToList(指定欄位) 八字不合,用起來有些麻煩。現在終於解決了!!~~ ```csharp var t111 = fsql.Select() .IncludeMany(a => a.TopicType.Photos) .Where(a => a.Id <= 100) .ToList(a => new { a.Id, a.TopicType.Photos, Photos2 = a.TopicType.Photos }); ``` ---- # 增加 $"{a.Code}_{a.Id}" lambda 解析; 在之前查詢資料的時候,$"" 這種語法糖神器居然不能使用在 lambda 表示式中,實屬遺憾。現在終於可以了,如下: ```csharp var item = fsql.GetRepository().Insert(new Topic { Clicks = 101, Title = "我是中國人101", CreateTime = DateTime.Parse("2020-7-5") }); var sql = fsql.Select().WhereDynamic(item).ToSql(a => new { str = $"x{a.Id + 1}z-{a.CreateTime.ToString("yyyyMM")}{a.Title}{a.Title}" }); Assert.Equal($@"SELECT concat('x',ifnull((a.`Id` + 1), ''),'z-',ifnull(date_format(a.`CreateTime`,'%Y%m'), ''),'',ifnull(a.`Title`, ''),'',ifnull(a.`Title`, ''),'') as1 FROM `tb_topic` a WHERE (a.`Id` = {item.Id})", sql); ``` > 再次說明:都是親兒子,並且都有對應的單元測試,兄臺大可放心用在不同的資料庫中 ---- # 增加 lambda 表示式樹解析子查詢 ToList + string.Join() 產生 類似 group_concat 的效果; v1.8.0+ string.Join + ToList 實現將子查詢的多行結果,拼接為一個字串,如:"1,2,3,4" ```csharp fsql.Select().ToList(a => new { id = a.Id, concat = string.Join(",", fsql.Select().ToList(b => b.Id)) }); //SELECT a.`Id`, (SELECT group_concat(b.`Id` separator ',') // FROM `StringJoin01` b) //FROM `Topic` a ``` 該語法,在不同資料庫都作了相應的 SQL 翻譯。 ---- # 增加 SqlExt 常用的自定義表示式樹解析; SqlExt.cs 定義了一些常用的表示式樹解析,如下: ```csharp fsql.Select() .InnerJoin((a, b) => b.Id == a.Id) .ToList((a, b) => new { Id = a.Id, EdiId = b.Id, over1 = SqlExt.Rank().Over().OrderBy(a.Id).OrderByDescending(b.EdiId).ToValue(), case1 = SqlExt.Case() .When(a.Id == 1, 10) .When(a.Id == 2, 11) .When(a.Id == 3, 12) .When(a.Id == 4, 13) .When(a.Id == 5, SqlExt.Case().When(b.Id == 1, 10000).Else(999).End()) .End(), //這裡因為複雜才這樣,一般使用三元表示式即可:a.Id == 1 ? 10 : 11 groupct1 = SqlExt.GroupConcat(a.Id).Distinct().OrderBy(b.EdiId).Separator("_").ToValue() }); ``` > 本功能利用 FreeSql 自定義解析實現常用表示式樹解析,歡迎 PR 補充 ---- # 增加 ISelect/IInsert/IUpdate/IDelete CommandTimeout 方法設定命令超時; 現在每條 crud 都可以設定命令執行的超時值,如下: ```csharp fsql.Insert().Where(...).CommandTimeout(60).ExecuteAffrows(); fsql.Update() .Set(a => a.Clicks + 1) .Where(...) .CommandTimeout(60).ExecuteAffrows(); fsql.Select().Where(...).CommandTimeout(60).ToList(); ``` ---- # 完善 WhereDynamicFilter 動態過濾查詢 ![](https://img2020.cnblogs.com/blog/31407/202005/31407-20200524235718959-1427957349.png) 是否見過這樣的高階查詢功能,WhereDynamicFilter 在後端可以輕鬆完成這件事情,前端根據 UI 組裝好對應的 json 字串傳給後端就行,如下: ```csharp DynamicFilterInfo dyfilter = JsonConvert.DeserializeObject(@" { ""Logic"" : ""Or"", ""Filters"" : [ { ""Field"" : ""Code"", ""Operator"" : ""NotContains"", ""Value"" : ""val1"", ""Filters"" : [{ ""Field"" : ""Name"", ""Operator"" : ""NotStartsWith"", ""Value"" : ""val2"" }] }, { ""Field"" : ""Parent.Code"", ""Operator"" : ""Equals"", ""Value"" : ""val11"", ""Filters"" : [{ ""Field"" : ""Parent.Name"", ""Operator"" : ""Contains"", ""Value"" : ""val22"" }] } ] }"); fsql.Select().WhereDynamicFilter(dyfilter).ToList(); //SELECT a.""Code"", a.""Name"", a.""ParentCode"", a__Parent.""Code"" as4, a__Parent.""Name"" as5, a__Parent.""ParentCode"" as6 //FROM ""D_District"" a //LEFT JOIN ""D_District"" a__Parent ON a__Parent.""Code"" = a.""ParentCode"" //WHERE (not((a.""Code"") LIKE '%val1%') AND not((a.""Name"") LIKE 'val2%') OR a__Parent.""Code"" = 'val11' AND (a__Parent.""Name"") LIKE '%val22%') ``` ISelect.WhereDynamicFilter 方法實現動態過濾條件(與前端互動),支援的操作符: - 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) ---- # 增加 BeginEdit/EndEdit 批量編輯資料的功能; 場景:winform 載入表資料後,一頓新增、修改、刪除操作之後,點選【儲存】 ```csharp [Fact] public void BeginEdit() { fsql.Delete().Where("1=1").ExecuteAffrows(); var repo = fsql.GetRepository(); var cts = new[] { new BeginEdit01 { Name = "分類1" }, new BeginEdit01 { Name = "分類1_1" }, new BeginEdit01 { Name = "分類1_2" }, new BeginEdit01 { Name = "分類1_3" }, new BeginEdit01 { Name = "分類2" }, new BeginEdit01 { Name = "分類2_1" }, new BeginEdit01 { Name = "分類2_2" } }.ToList(); repo.Insert(cts); repo.BeginEdit(cts); //開始對 cts 進行編輯 cts.Add(new BeginEdit01 { Name = "分類2_3" }); cts[0].Name = "123123"; cts.RemoveAt(1); Assert.Equal(3, repo.EndEdit()); } class BeginEdit01 { public Guid Id { get; set; } public string Name { get; set; } } ``` 上面的程式碼 EndEdit 方法執行的時候產生 3 條 SQL 如下: ```sql INSERT INTO "BeginEdit01"("Id", "Name") VALUES('5f26bf07-6ac3-cbe8-00da-7dd74818c3a6', '分類2_3') UPDATE "BeginEdit01" SET "Name" = '123123' WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd01be76e26') DELETE FROM "BeginEdit01" WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd11bcf54dc') ``` 提醒:該操作只對變數 cts 有效,不是針對全表對比更新。 ---- # 增加 父子表(樹表)遞迴查詢、刪除功能; 無限級分類(父子)是一種比較常用的表設計,每種設計方式突出優勢的同時也帶來缺陷,如: - 方法1:表設計中只有 parent_id 欄位,困擾:查詢麻煩(本文可解決); - 方法2:表設計中冗餘子級id便於查詢,困擾:新增/更新/刪除的時候需要重新計算; - 方法3:表設計中儲存左右值編碼,困擾:同上; 方法1設計最簡單,我們正是解決它設計簡單,使用複雜的問題。 首先,按照導航屬性的定義,定義好父子屬性: ```csharp public class Area { [Column(IsPrimary = true)] public string Code { get; set; } public string Name { get; set; } public virtual string ParentCode { get; set; } [Navigate(nameof(ParentCode))] public Area Parent { get; set; } [Navigate(nameof(ParentCode))] public List Childs { get; set; } } ``` 定義 Parent 屬性,在表示式中可以這樣: ```csharp fsql.Select().Where(a => a.Parent.Parent.Parent.Name == "中國").First(); ``` 定義 Childs 屬性,在表示式中可以這樣(子查詢): ```csharp fsql.Select().Where(a => a.Childs.AsSelect().Any(c => c.Name == "北京")).First(); ``` 定義 Childs 屬性,還可以使用[【級聯儲存】](https://github.com/dotnetcore/FreeSql/wiki/%E8%81%94%E7%BA%A7%E4%BF%9D%E5%AD%98)、[【貪婪載入】](https://github.com/2881099/FreeSql/wiki/%e8%b4%aa%e5%a9%aa%e5%8a%a0%e8%bd%bd) 等等操作。 利用級聯儲存,新增測試資料如下: ```csharp fsql.Delete().Where("1=1").ExecuteAffrows(); var repo = fsql.GetRepository(); repo.DbContextOptions.EnableAddOrUpdateNavigateList = true; repo.DbContextOptions.NoneParameter = true; repo.Insert(new Area { Code = "100000", Name = "中國", Childs = new List(new[] { new Area { Code = "110000", Name = "北京", Childs = new List(new[] { new Area{ Code="110100", Name = "北京市" }, new Area{ Code="110101", Name = "東城區" }, }) } }) }); ``` > 功能1:ToTreeList 配置好父子屬性之後,就可以這樣用了: ```csharp var t1 = fsql.Select().ToTreeList(); Assert.Single(t1); Assert.Equal("100000", t1[0].Code); Assert.Single(t1[0].Childs); Assert.Equal("110000", t1[0].Childs[0].Code); Assert.Equal(2, t1[0].Childs[0].Childs.Count); Assert.Equal("110100", t1[0].Childs[0].Childs[0].Code); Assert.Equal("110101", t1[0].Childs[0].Childs[1].Code); ``` 查詢資料本來是平面的,ToTreeList 方法將返回的平面資料在記憶體中加工為樹型 List 返回。 > 功能2:AsTreeCte 遞迴刪除 很常見的無限級分類表功能,刪除樹節點時,把子節點也處理一下。 ```csharp fsql.Select() .Where(a => a.Name == "中國") .AsTreeCte() .ToDelete() .ExecuteAffrows(); //刪除 中國 下的所有記錄 ``` 如果軟刪除: ```csharp fsql.Select() .Where(a => a.Name == "中國") .AsTreeCte() .ToUpdate() .Set(a => a.IsDeleted, true) .ExecuteAffrows(); //軟刪除 中國 下的所有記錄 ``` > 功能3:AsTreeCte 遞迴查詢 若不做資料冗餘的無限級分類表設計,遞迴查詢少不了,AsTreeCte 正是解決遞迴查詢的封裝,方法引數說明: | 引數 | 描述 | | -- | -- | | (可選) pathSelector | 路徑內容選擇,可以設定查詢返回:中國 -> 北京 -> 東城區 | | (可選) up | false(預設):由父級向子級的遞迴查詢,true:由子級向父級的遞迴查詢 | | (可選) pathSeparator | 設定 pathSelector 的連線符,預設:-> | | (可選) level | 設定遞迴層級 | > 通過測試的資料庫:MySql8.0、SqlServer、PostgreSQL、Oracle、Sqlite、達夢、人大金倉 姿勢一:AsTreeCte() + ToTreeList ```csharp var t2 = fsql.Select() .Where(a => a.Name == "中國") .AsTreeCte() //查詢 中國 下的所有記錄 .OrderBy(a => a.Code) .ToTreeList(); //非必須,也可以使用 ToList(見姿勢二) Assert.Single(t2); Assert.Equal("100000", t2[0].Code); Assert.Single(t2[0].Childs); Assert.Equal("110000", t2[0].Childs[0].Code); Assert.Equal(2, t2[0].Childs[0].Childs.Count); Assert.Equal("110100", t2[0].Childs[0].Childs[0].Code); Assert.Equal("110101", t2[0].Childs[0].Childs[1].Code); // WITH "as_tree_cte" // as // ( // SELECT 0 as cte_level, a."Code", a."Name", a."ParentCode" // FROM "Area" a // WHERE (a."Name" = '中國') // union all // SELECT wct1.cte_level + 1 as cte_level, wct2."Code", wct2."Name", wct2."ParentCode" // FROM "as_tree_cte" wct1 // INNER JOIN "Area" wct2 ON wct2."ParentCode" = wct1."Code" // ) // SELECT a."Code", a."Name", a."ParentCode" // FROM "as_tree_cte" a // ORDER BY a."Code" ``` 姿勢二:AsTreeCte() + ToList ```csharp var t3 = fsql.Select() .Where(a => a.Name == "中國") .AsTreeCte() .OrderBy(a => a.Code) .ToList(); Assert.Equal(4, t3.Count); Assert.Equal("100000", t3[0].Code); Assert.Equal("110000", t3[1].Code); Assert.Equal("110100", t3[2].Code); Assert.Equal("110101", t3[3].Code); //執行的 SQL 與姿勢一相同 ``` 姿勢三:AsTreeCte(pathSelector) + ToList 設定 pathSelector 引數後,如何返回隱藏欄位? ```csharp var t4 = fsql.Select() .Where(a => a.Name == "中國") .AsTreeCte(a => a.Name + "[" + a.Code + "]") .OrderBy(a => a.Code) .ToList(a => new { item = a, level = Convert.ToInt32("a.cte_level"), path = "a.cte_path" }); Assert.Equal(4, t4.Count); Assert.Equal("100000", t4[0].item.Code); Assert.Equal("110000", t4[1].item.Code); Assert.Equal("110100", t4[2].item.Code); Assert.Equal("110101", t4[3].item.Code); Assert.Equal("中國[100000]", t4[0].path); Assert.Equal("中國[100000] -> 北京[110000]", t4[1].path); Assert.Equal("中國[100000] -> 北京[110000] -> 北京市[110100]", t4[2].path); Assert.Equal("中國[100000] -> 北京[110000] -> 東城區[110101]", t4[3].path); // WITH "as_tree_cte" // as // ( // SELECT 0 as cte_level, a."Name" || '[' || a."Code" || ']' as cte_path, a."Code", a."Name", a."ParentCode" // FROM "Area" a // WHERE (a."Name" = '中國') // union all // SELECT wct1.cte_level + 1 as cte_level, wct1.cte_path || ' -> ' || wct2."Name" || '[' || wct2."Code" || ']' as cte_path, wct2."Code", wct2."Name", wct2."ParentCode" // FROM "as_tree_cte" wct1 // INNER JOIN "Area" wct2 ON wct2."ParentCode" = wct1."Code" // ) // SELECT a."Code" as1, a."Name" as2, a."ParentCode" as5, a.cte_level as6, a.cte_path as7 // FROM "as_tree_cte" a // ORDER BY a."Code" ``` > 更多姿勢...請根據程式碼註釋進行嘗試 # 寫在最後 給 .NET 開源社群貢獻一點力時,希望作者的努力能打動到你,請求正在使用的、善良的您能動一動小手指,把文章轉發一下,讓更多人知道 .NET 有這樣一個好用的 ORM 存在。謝謝了!! FreeSql 使用最寬鬆的開源協議 MIT [https://github.com/dotnetcore/FreeSql](https://github.com/dotnetcore/FreeSql),完全可以商用,文件齊全。QQ群:4336577(已滿)、8578575(線上)、52508226(線上) 如果你有好的 ORM 實現想法,歡迎給作者留言討論,謝謝觀看! 2.0 版本意見正在登記中:https://github.com/dotnetcore/FreeSql/issues/469