1. 程式人生 > >.NET ORM 開源專案 FreeSql 1.0 正式版釋出

.NET ORM 開源專案 FreeSql 1.0 正式版釋出

一、簡介

FreeSql 是 .NET 平臺下的物件關係對映技術(O/RM),支援 .NetCore 2.1+ 或 .NetFramework 4.0+ 或 Xamarin。

從 0.0.1 釋出,歷時整整一年的迭代更新,原計劃元旦釋出1.0,可能作者比較急提前了幾天釋出。其實是元旦有其他事……

本文內容從簡,介紹專案的主要功能框架,以及暫時能想到的可能比較有說服力的特性。

二、專案統計

主倉庫解決方案共計專案:29個

單元測試:3510個

Code Issues:168個

文件Wiki:43個

Stars:1140

Forks:236

Commits:690次

Nuget主包下載量:86,568次

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

三、功能結構

  • 支援 CodeFirst 遷移,哪怕使用 Access 資料庫也支援;
  • 支援 DbFirst 從資料庫匯入實體類;
  • 支援 深入的型別對映,比如pgsql的陣列型別;
  • 支援 豐富的表示式函式,以及靈活的自定義解析;
  • 支援 導航屬性一對多、多對多貪婪載入,以及延時載入;
  • 支援 讀寫分離、分表分庫,租戶設計,過濾器,樂觀鎖,悲觀鎖;
  • 支援 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/達夢資料庫/Access;

四、CodeFirst/DbFirst

一切皆 CodeFirst,所有功能都是由實體型別,到表操作的過程。CodeFirst 【自動遷移】只需要一行程式碼:

using FreeSql;

static IFreeSql fsql = new FreeSqlBuilder()
    .UseConnectionString(DataType.Sqlite, 
        @"Data Source=|DataDirectory|\document.db;Pooling=true;Max Pool Size=10")
    .UseAutoSyncStructure(true) //自動同步實體結構到資料庫
    .Build();

在開發過程中,表結構會自動建立、或改變(不丟資料),取決於實體類的變化。

CodeFirst 提供功能豐富的特性ColumnAttribute,定義實體與表間的對映,並且支援 FluentApi 方式。如果不喜歡 ColumnAttribute 這個名字,還可以通過 AOP 設定換為 MyColumnAttribute。

using FreeSql.DataAnnotations;

class Song {
    [Column(IsIdentity = true)]
    public int Id { get; set; }
    public string Title { get; set; }
    public string Url { get; set; }
    public DateTime CreateTime { get; set; }
}

DbFirst 資料表先行,許多哥們使用動軟、T4模板生成實體類程式碼。自已處理每種資料庫的欄位型別,和 csharp 型別對應,比較麻煩,各大 ORM 可能還不通用。

我們提供命令列工具生成實體類,dotnet-tools,對就是它。。非常好用的工具,沒有之一。

C:\Users\28810>dotnet tool install -g freesql.generator
可使用以下命令呼叫工具: FreeSql.Generator
已成功安裝工具“freesql.generator”(版本“1.0.0”)。

C:\Users\28810>FreeSql.Generator —help

它基於 Razor 模板生成,支援自定義模板生成,意味著它遠不止可以生成實體類,甚至是 IRepository 或者。。。

五、導航屬性

從一開始就著重導航物件的設計,支援一對多、多對多、父子關係、一對一、多對一,不誇張的說目前對導航屬性處理最流弊,最容易上手的 ORM。多表查詢的表示式使用非常便利,如下:

fsql.Select<Catetory>()
    .Where(a => a.Parent.Parent.Name == "粵語")

可以使用導航屬性一直這樣點下去。。。

級聯儲存,級聯查詢功能也必不可少,如下查詢多對多:

fsql.Select<Song>()
    .IncludeMany(a => a.Tags)
    .ToList();

上面的程式碼,如果只返回 Tags 前 5條記錄,也是支援的 .IncludeMany(a => a.Tags.Take(5))

對效能有追求,還可以指定 Tags 只查詢部分欄位

關於 IncludeMany 不便再這過多展開介紹。。。(其實還有黑科技!)

哦,還有 FreeSql.AdminLTE 擴充套件包,它不屬於主倉庫專案,最大化利用導航屬性完成通用的 CURD 後臺管理功能。

流弊噠噠~~~~

六、倉儲模式

倉儲工作單元目前是當下的流行風,在比較早的時候大約0.2版本釋出了第一個倉儲版本,當時參考了大量的專案設計,最終選用 abp vnext 的 IRepository 設計介面,實現通用倉儲類功能。

也就是說,使用 FreeSql.Repository 你不必再自己寫那些繁瑣的 CURD 重複的倉儲功能,不用再頭疼倉儲類的介面方法定義。定義標準比寫程式碼難多了,abp vnext 的 IRepository 目前是見過最好的,木有之一!!

倉儲模式都在操作實體物件,無論是更新還是刪除,都是傳物件。。。傳傳傳。。。

問題1、傳物件更新,意味著更新所有欄位?

不會的,我們的倉儲實現擁有狀態管理機制,從物件查詢出來的時候已經記錄了拍照,當呼叫更新方法的時候會與之對比,計算出變化的欄位,只更新變化的欄位!

var repo = fsql.GetRepository<Song>();
var item = repo.Where(a => a.Id == 1).First();
item.Title = "原諒我今天";
repo.Update(item);

提示:支援樂觀鎖、悲觀鎖

問題2、狀態管理是否影響效能?

不完全,因為狀態管理設計在倉儲實現之上,我們最原始的 IFreeSql 沒有這個功能(倉儲算是一種擴充套件包吧,但是倉儲又非常有效)。倉儲即用即銷燬,擅用它的對比功能更新物件,不濫用沒有效能問題。

有了倉儲怎麼會沒有 UnitOfWork 呢,UnitOfWork 目前以事務的方式做了預設實現,並且它擁有實體變化跟蹤記錄。

七、效能

1、插入測試(52個欄位)

18W 1W 5K 2K 1K 500 100 50
MySql 5.5 ExecuteAffrows 55,497 4,953 2,304 2,554 1,516 1,572 265 184
SqlServer Express ExecuteAffrows 402,355 24,847 11,465 4,971 2,437 915 138 88
SqlServer Express ExecuteSqlBulkCopy 21,065 578 326 139 105 79 60 48
PostgreSQL 10 ExecuteAffrows 46,756 3,294 2,269 1,019 374 209 51 37
PostgreSQL 10 ExecutePgCopy 10,090 583 337 136 88 61 30 25
Oracle XE ExecuteAffrows - - - - 24,528 10,648 571 200
Sqlite ExecuteAffrows 28,554 1,149 701 327 155 91 44 35

測試結果,是在相同作業系統下進行的,並且都有預熱

18W 解釋:插入18萬行記錄,表格中的數字是執行時間(單位ms)

Oracle 插入效能不用懷疑,可能安裝學生版限制較大

提醒:開源資料庫測試結果比較有意義,商業資料庫版本之間效能可能有較大差距

2、插入測試(10個欄位)

18W 1W 5K 2K 1K 500 100 50
MySql 5.5 ExecuteAffrows 15,380 1,813 1,457 1,254 563 246 55 21
SqlServer Express ExecuteAffrows 47,204 2,275 1,108 488 279 123 35 16
SqlServer Express ExecuteSqlBulkCopy 4,248 127 71 30 48 14 11 10
PostgreSQL 10 ExecuteAffrows 9,786 568 336 157 102 34 9 6
PostgreSQL 10 ExecutePgCopy 4,081 167 93 39 21 12 4 2
Oracle XE ExecuteAffrows - - - - 2,394 731 67 33
Sqlite ExecuteAffrows 4,524 246 137 94 35 19 14 11

提示:已經支援了 SqlServer 資料庫的 SqlBulkCopy 功能、以及 PostgreSQL 資料庫的 Copy 功能

八、拉姆達

非常特色的功能之一,深入細化函式解析,所支援的型別基本都可以使用對應的表示式函式,例如 日期、字串、IN查詢、陣列(PostgreSQL的陣列)、字典(PostgreSQL HStore)等等。

1、In查詢

var t1 = fsql.Select<T>()
  .Where(a => new[] { 1, 2, 3 }.Contains(a.Id))
  .ToSql();
//SELECT .. FROM ..
//WHERE (a.`Id` in (1,2,3))

已優化,防止 where in 元素多過的 SQL 錯誤,如:

[Err] ORA-01795: maximum number of expressions in a list a 1000

原來:where id in (1..1333)

現在:where id in (1..500) or id in (501..1000) or id in (1001..1333)

2、In查詢(多列)

//元組集合
vae lst = new List<(Guid, DateTime)>();

lst.Add((Guid.NewGuid(), DateTime.Now));
lst.Add((Guid.NewGuid(), DateTime.Now));
lst.Add((Guid.NewGuid(), DateTime.Now));
fsql.Select<T>()
  .Where(a => lst.Contains(a.Id, a.ct1))
  .ToSql();
//SELECT .. FROM ..
//WHERE (a."Id" = '685ee1f6-bdf6-4719-a291-c709b8a1378f' AND a."ct1" = '2019-12-07 23:55:27' OR 
//a."Id" = '5ecd838a-06a0-4c81-be43-1e77633b7404' AND a."ct1" = '2019-12-07 23:55:27' OR 
//a."Id" = 'b8b366f3-1c03-4547-9c96-d362dd5cae6a' AND a."ct1" = '2019-12-07 23:55:27')

3、自定義函式

預設已經支援了很豐富的函式解析,如果不夠再自己定義:

[ExpressionCall]
public static class DbFunc
{
    //必要定義 static + ThreadLocal
    static ThreadLocal<ExpressionCallContext> context = new ThreadLocal<ExpressionCallContext>();

    public static DateTime FormatDateTime(this DateTime that, string arg1)
    {
        var up = context.Value;
        if (up.DataType == FreeSql.DataType.Sqlite) //重寫內容
            context.Value.Result = $"date_format({up.ParsedContent["that"]}, {up.ParsedContent["arg1"]})";
        return that;
    }
}

fsql.Select<T>().ToSql(a => a.CreateTime.FormatDateTime("yyyy-MM-dd"));
//SELECT date_format(a."CreateTime", 'yyyy-MM-dd') as1 
//FROM "T" a

提示:SqlServer nvarchar/varchar 已兼容表達式解析,分別解析為:N'' 和 '',優化索引執行計劃

九、騷操作

1、程式碼註釋 -> 遷移到資料庫

CodeFirst 支援將 c# 程式碼內的註釋,遷移至資料庫的備註。先決條件:

  • 實體類所在程式集,需要開啟 xml 文件功能;
  • xml 檔案必須與程式集同目錄,且檔名:xxx.dll -> xxx.xml;

2、NoneParameter

可以設定不使用 引數化 執行 SQL 命令,方便開發除錯,區別如下:

INSERT INTO `tb_topic`(`Title`) VALUES(?Title0)
INSERT INTO `tb_topic`(`Title`) VALUES('Title_1')

在 new FreeSqlBuilder().UseNoneParameter(true) 全域性設定

在 單次 ISelect、IInsert、IDelete、IUpdate 上使用 NoneParameter() 設定單次生效

3、Dto 對映查詢

用過 ProjectTo 功能嗎?沒用過當忽略此行。。。

有些朋友可能是先 ToList().Mapper<T>(),這樣會先查詢了所有欄位。

Dto 對映查詢支援單表/多表,這個功能可以決定只查詢部分欄位(不是、不是、不是先查詢所有欄位再到記憶體對映)。

規則:查詢屬性名,會迴圈內部物件 _tables(多表會增長),以 主表優先查,直到查到相同的欄位。

如:A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被對映。也可以指定 id = C.id 對映。

fsql.Select<Song>().ToList(a => new DTO { xxx = a.ext }) 
//情況1:附加所有對映,再額外對映 ext,返回 List<DTO>

fsql.Select<Song>().ToList(a => new Song { id = a.id }) 
//情況2:只查詢 id,返回 List<Song>

fsql.Select<Song>().ToList(a => new { id = a.id }) 
//情況3:只查詢 id,返回 List<匿名物件>

fsql.Select<Song>().ToList(a => new DTO(a.id))
//情況4:只查詢 id,返回 List<DTO>

fsql.Select<Song>().ToList(a => new DTO(a.id) { xxx = a.ext })
//情況5:查詢 id, ext,返回 List<DTO>

fsql.Select<Song>().ToList(a => new Song(a.id))
//情況6:查詢 id,返回 List<Song>

fsql.Select<Song>().ToList(a => new Song(a.id) { xxx = a.ext })
//情況7:查詢 id, ext,返回 List<Song>

4、WhereCascade

FreeSql 擅長多表查詢,遇到像isdeleted每個表都給條件的時候,挺麻煩。WhereCascade使用後生成sql時,所有表都附上這個條件。

如:

fsql.Select<t1>()
    .LeftJoin<t2>(...)
    .WhereCascade(x => x.IsDeleted == false)
    .ToList();

得到的 SQL:

SELECT ...
FROM t1
LEFT JOIN t2 on ... AND (t2.IsDeleted = 0) 
WHERE t1.IsDeleted = 0

其中的實體可附加表示式時才生效,支援子表查詢。單次查詢使用的表數目越多收益越大。

5、審計 CURD

如果因為某個 sql 騷操作耗時很高,沒有一個相關的審計功能,排查起來可以說無從下手。

FreeSql 支援簡單的類似功能:

fsql.Aop.CurdAfter = (s, e) => {
    if (e.ElapsedMilliseconds > 200) {
        //記錄日誌
        //傳送簡訊給負責人
    }
};

只需要一個事件,就可以對全域性起到作用。

還有一個 CurdBefore 在執行 sql 之前觸發,常用於記錄日誌或開發除錯。

6、審計屬性值

實現插入/更新時統一處理某些值,比如某屬性的雪花演算法值、建立時間值、甚至是業務值。

fsql.Aop.AuditValue += (s, e) => {
    if (e.Column.CsType == typeof(long) 
        && e.Property.GetCustomAttribute<SnowflakeAttribute>(false) != null
        && e.Value?.ToString() == 0)
        e.Value = new Snowflake().GetId();
};

class Order {
    [Snowflake]
    public long Id { get; set; }
    //...
}

當屬性的型別是 long,並且標記了 [Snowflake],並且當前值是 0,那麼在插入/更新時它的值將設定為雪花id值。

說明:SnowflakeAttribute 是使用者您來定義,new Snowflake().GetId() 也是由使用者您來實現

如果命名規範,可以在 aop 裡判斷,if (e.Property.Name == "createtime") e.Value = DateTime.Now;

還有。。還有很多騷操作。。不便在此展開。。。

十、展望 2020

2019 年支援了主流的資料庫:

  • SqlServer 2000-2019,支援 row_number/offset fetch next 分頁自動版本選擇適配,以及其他語法的差異適配,提供 ado.net 與 odbc 兩種實現方式;

  • PostgreSQL 9.4-12,完成了版本間部分差異適配,提供 ado.net 與 odbc 兩種實現方式;

  • MySql 5.5、Mariadb,提供 Oracle 官方驅動、與 MySqlConnector 社群驅動,還有 odbc 實現方式;

  • Oracle 11+,提供 ado.net 與 odbc 兩種實現方式;

  • Sqlite,相容了 .net core / .net framework / xamarin 平臺適配,支援 CodeFirst 開發模式,一個字爽!!!

  • MsAccess 2003-2007,提供 oledb 實現方式,支援 CodeFirst 開發模式;

  • 達夢,提供 odbc 的實現方式,並且支援 DbFirst 和 CodeFirst 兩種開發模式;

2020 年支援國產是重點,重心,重要的工作內容,南大通用將是下一個目標,並且已經在進行中了。

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

寫到最後面,感謝這一年來與 FreeSql 一直陪伴的兄弟朋友們。