.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 一直陪伴的兄弟朋友們。

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