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