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

寫在開頭

FreeSql 是 .NET 開源生態下的 ORM 輪子,轉眼快兩年了,說真的開源不容易(只有經歷過才明白)。今天帶點乾貨和濕貨給大家,先說下濕貨。

認識我的人,知道 CSRedisCore 是我寫的另外一個開源組件,這個項目是 2016 年從 ctstone/csredis 項目 clone 到自己工作的項目中,修改源碼經過一年多生產考慮於 2017 年發布開源於 //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 到現在,2300 顆星,500 Issues,200K 包下載量。說明還是有開發者關注和喜愛,只要有人關注,就不會停更不修 BUG 一說。大家有興趣可以看看更新記錄,看看我們的程式碼提交量,5400+ 單元測試不說非常多,個人覺得已經超過很多國產項目。

23個月了,FreeSql 還活著,而且生命力頑強見下圖:

年底發布 2.0 版本正在收集需求中(歡迎前去 issues 誠意登記),本文將介紹在過去的幾個月完成的一些有意義的功能介紹。

FreeSql 是 .Net ORM,能支援 .NetFramework4.0+、.NetCore、Xamarin、XAUI、Blazor、以及還有說不出來的運行平台,因為程式碼綠色無依賴,支援新平台非常簡單。目前單元測試數量:5400+,Nuget下載數量:200K+,源碼幾乎每天都有提交。值得高興的是 FreeSql 加入了 ncc 開源社區://github.com/dotnetcore/FreeSql,加入組織之後社區責任感更大,需要更努力做好品質,為開源社區出一份力。

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

為什麼要重複造輪子?

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 批量編輯數據的功能;

十二、增加 父子表(樹表)遞歸查詢、刪除功能;

FreeSql 使用非常簡單,只需要定義一個 IFreeSql 對象即可:

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 其他全部是親兒子,在功能提供方面一碗水端平。

眾所周知 EFCore for oracle 問題多,並且現在才剛剛更新到 3.x,在這樣的背景下,一個國產資料庫更不能指望誰實現好用的 EFCore。目前看來除了 EFCore for sqlserver 我們沒把握完全佔優勢,起碼在其他資料庫肯定是我們更接地氣。

使用 FreeSql 訪問人大金倉/神通 資料庫,只需要修改程式碼如下即可:

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.ShenTong, connectionString) //修改 DataType 設置切換資料庫
    .UseAutoSyncStructure(true) //自動同步實體結構到資料庫
    .Build(); //請務必定義成 Singleton 單例模式

增加 GlobalFilter.ApplyIf 創建動態過濾器;

FreeSql 使用全局過濾器非常簡單,我們的過濾器支援多表查詢、子查詢,只需要設置一次:

public static AsyncLocal<Guid> TenantId { get; set; } = new AsyncLocal<Guid>();

fsql.GlobalFilter
    .Apply<ISoftDelete>("name1", a => a.IsDeleted == false)
    .ApplyIf<ITenant>("tenant", () => TenantId.Value != Guid.Empty, a => a.TenantId == TenantId.Value);

上面增加了兩個過濾器,tenant 第二個參數正是增加的功能,當委託條件成立時才會附加過濾器。


增加 ISelect.InsertInto 將查詢轉換為 INSERT INTO t1 SELECT … FROM t2 執行插入;

int affrows = fsql.Select<Topic>()
  .Limit(10)
  .InsertInto(null, a => new Topic2
  {
    Title = a.Title
  });
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() 相當於批量插入
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<T>().SetSource(數組).ExecuteAffrows() 相當於批量更新


增加 IncludeMany(a => a.Childs).ToList(a => new { a.Childs }) 指定集合屬性返回;

這個功能實在太重要了,在此之前 IncludeMany 和 ToList(指定欄位) 八字不合,用起來有些麻煩。現在終於解決了!!~~

var t111 = fsql.Select<TestInclude_OneToManyModel1>()
    .IncludeMany(a => a.model2.childs.Where(m3 => m3.model2111Idaaa == a.model2.model2id))
    .Where(a => a.id <= model1.id)
    .ToList(a => new
    {
        a.id,
        a.model2.childs,
        childs2 = a.model2.childs
    });

增加 $”{a.Code}_{a.Id}” lambda 解析;

在之前查詢數據的時候,$”” 這種語法糖神器居然不能使用在 lambda 表達式中,實屬遺憾。現在終於可以了,如下:

var item = fsql.GetRepository<Topic>().Insert(new Topic { Clicks = 101, Title = "我是中國人101", CreateTime = DateTime.Parse("2020-7-5") });
var sql = fsql.Select<Topic>().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″

fsql.Select<Topic>().ToList(a => new {
  id = a.Id,
  concat = string.Join(",", fsql.Select<StringJoin01>().ToList(b => b.Id))
});
//SELECT a.`Id`, (SELECT group_concat(b.`Id` separator ',') 
//    FROM `StringJoin01` b) 
//FROM `Topic` a

該語法,在不同資料庫都作了相應的 SQL 翻譯。

增加 SqlExt 常用的自定義表達式樹解析;

SqlExt.cs 定義了一些常用的表達式樹解析,如下:

fsql.Select<T1, T2>()
  .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 都可以設置命令執行的超時值,如下:

fsql.Insert<items).CommandTimeout(60).ExecuteAffrows();

fsql.Delete<T>().Where(...).CommandTimeout(60).ExecuteAffrows();

fsql.Update<T>()
    .Set(a => a.Clicks + 1)
    .Where(...)
    .CommandTimeout(60).ExecuteAffrows();

fsql.Select<T>().Where(...).CommandTimeout(60).ToList();

完善 WhereDynamicFilter 動態過濾查詢

是否見過這樣的高級查詢功能,WhereDynamicFilter 在後端可以輕鬆完成這件事情,前端根據 UI 組裝好對應的 json 字元串傳給後端就行,如下:

DynamicFilterInfo dyfilter = JsonConvert.DeserializeObject<DynamicFilterInfo>(@"
{
  ""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<VM_District_Parent>().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 載入表數據後,一頓添加、修改、刪除操作之後,點擊【保存】

[Fact]
public void BeginEdit()
{
    fsql.Delete<BeginEdit01>().Where("1=1").ExecuteAffrows();
    var repo = fsql.GetRepository<BeginEdit01>();
    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 如下:

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設計最簡單,我們正是解決它設計簡單,使用複雜的問題。

首先,按照導航屬性的定義,定義好父子屬性:

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<Area> Childs { get; set; }
}

定義 Parent 屬性,在表達式中可以這樣:

fsql.Select<Area>().Where(a => a.Parent.Parent.Parent.Name == "中國").First();

定義 Childs 屬性,在表達式中可以這樣(子查詢):

fsql.Select<Area>().Where(a => a.Childs.AsSelect().Any(c => c.Name == "北京")).First();

定義 Childs 屬性,還可以使用【級聯保存】【貪婪載入】 等等操作。

利用級聯保存,添加測試數據如下:

fsql.Delete<Area>().Where("1=1").ExecuteAffrows();
var repo = fsql.GetRepository<Area>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = true;
repo.DbContextOptions.NoneParameter = true;
repo.Insert(new Area
{
  Code = "100000",
  Name = "中國",
  Childs = new List<Area>(new[] {
    new Area
    {
      Code = "110000",
      Name = "北京",
      Childs = new List<Area>(new[] {
        new Area{ Code="110100", Name = "北京市" },
        new Area{ Code="110101", Name = "東城區" },
      })
    }
  })
});

功能1:ToTreeList

配置好父子屬性之後,就可以這樣用了:

var t1 = fsql.Select<Area>().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 遞歸刪除

很常見的無限級分類表功能,刪除樹節點時,把子節點也處理一下。

fsql.Select<Area>()
  .Where(a => a.Name == "中國")
  .AsTreeCte()
  .ToDelete()
  .ExecuteAffrows(); //刪除 中國 下的所有記錄

如果軟刪除:

fsql.Select<Area>()
  .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

var t2 = fsql.Select<Area>()
  .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

var t3 = fsql.Select<Area>()
  .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 參數後,如何返回隱藏欄位?

var t4 = fsql.Select<Area>()
  .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 //github.com/dotnetcore/FreeSql,完全可以商用,文檔齊全。QQ群:4336577(已滿)、8578575(在線)、52508226(在線)

如果你有好的 ORM 實現想法,歡迎給作者留言討論,謝謝觀看!

2.0 版本意見正在登記中://github.com/dotnetcore/FreeSql/issues/469