[開源] .Net ORM FreeSql 1.8.0-preview 最新動態播報(番號:我還活着)

寫在開頭

FreeSql 是 .NET 開源生態下的 ORM 輪子,在一些人眼裏屬於重複造輪子:不看也罷。就像昨天有位朋友截圖某培訓直播發給我看,內容爲:“FreeSQL(個人產品),自己玩可以,不要商用。ORM框架:1.安全、穩定(更新穩定、有BUG有人修復,有人升級)”。

這突出其來的“關愛”,讓我的內心毫無波瀾,確實是毫無波瀾,比起當初 FreeSql 初出茅廬之時的諷刺友好得多。寫在開頭的這些內容並不祈求這部分人改變觀念,該黑的請繼續黑,黑總比沒有關注好,是吧?我無所謂你,但是別人呢?麻煩你們不要無腦抨擊,你們這種行爲不知道挽殺了多少社區項目。

2018 年 12 月份開發 FreeSql 到現在,1859 顆星,412 Issues,18 PR,170K 包下載量。說明還是有開發者關注和喜愛,只要有人關注,就不會停更不修 BUG 一說。大家有興趣可以看看更新記錄,看看我們的代碼提交量,4700+ 單元測試不說非常多,我個人覺得已經超過很多國產項目,有興趣的再去隔壁“國產第一” ORM 上看看,對比對比!如果不更新了,請把位置讓出來;如果有BUG修復不了,請讓 FreeSql 來;如果不好用,就不要搞一堆 SEO 害人入坑;如果。。。如果。。。

這不是挑釁,看到對方的 issues 實在不忍,看到對方的源碼,哇哦,單元測試在哪裏?好了不廢話了。。

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

預告:年底發佈 2.0.0 版本將凍結新功能開發,不再製造新 BUG,一心修復老功能引出的 BUG,完善文檔。

本文將介紹在過去的三個月完成的一些有意義的功能介紹。

入戲準備

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

QQ羣:4336577(已滿)、8578575(在線)

爲什麼要重複造輪子?

FreeSql 主要優勢在於易用性上,基本是開箱即用,在不同數據庫之間切換兼容性比較好。作者花了大量的時間精力在這個項目,肯請您花半小時瞭解下項目,謝謝。

FreeSql 整體的功能特性如下:

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

1.5.0 -> 1.8.0-preview 更新的重要功能如下:

一、增加 $"{a.Code}_{a.Id}" lambda 解析;

二、增加 lambda 表達式樹解析子查詢 ToList + string.Join() 產生 類似 group_concat 的效果;

三、增加 SqlExt 常用開窗函數的自定義表達式解析;

四、完善 WhereDynamicFilter 動態過濾查詢;

五、增加 BeginEdit/EndEdit 批量編輯數據的功能;

六、增加 人大金倉/神通 數據庫的訪問支持;

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

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

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.MySql, connectionString)
    .UseAutoSyncStructure(true) //自動同步實體結構到數據庫
    .Build(); //請務必定義成 Singleton 單例模式

增加 $"{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 補充

完善 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 有效,不是針對全表對比更新。

增加 人大金倉/神通 數據庫的訪問支持

天津神舟通用數據技術有限公司(簡稱“神舟通用公司”),隸屬於中國航天科技集團(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 單例模式

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

無限級分類(父子)是一種比較常用的表設計,每種設計方式突出優勢的同時也帶來缺陷,如:

  • 方法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 有這樣一個好用的 ORM 存在。謝謝了!!

FreeSql 開源協議 MIT https://github.com/dotnetcore/FreeSql,可以商用,文檔齊全。QQ羣:4336577(已滿)、8578575(在線)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章