天天看點

.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

三、功能結構

.NET ORM 開源項目 FreeSql 1.0 正式版釋出
  • 支援 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 背景管理功能。

.NET ORM 開源項目 FreeSql 1.0 正式版釋出

流弊哒哒~~~~

六、倉儲模式

倉儲工作單元目前是當下的流行風,在比較早的時候大約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 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個字段)

15,380 1,813 1,457 1,254 563 246 55 21
47,204 2,275 1,108 488 279 123 16
4,248 127 71 14 11 10
9,786 568 336 157 102 34 9 6
4,081 167 93 39 12 4 2
2,394 731 67 33
4,524 137 94 19

提示:已經支援了 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 年支援國産是重點,重心,重要的工作内容,南大通用将是下一個目标,并且已經在進行中了。

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

繼續閱讀