廢話開頭
這篇文章是我有史以來編輯最長時間的,歷時 4小時!!!原本我可以利用這 4小時編寫一堆膠水程式碼,真心希望善良的您點個贊,謝謝了!!
很久很久沒有寫文章了,上一次還是在元旦釋出 1.0 版本的時候,今年版本規劃是每月底釋出小版本(年底釋出 2.0),全年的開源工作主要是收集使用者需求增加功能,完善測試,修復 bug。FreeSql 1.0 -> 1.5 相隔半年有哪些新功能?只能說每個功能都能讓我興奮,並且能感受到使用者也一樣興奮(妄想症)。
迫不及待的人會問,這更新速度也太快了吧,升級會不會有問題?
- 不瞭解版本的更新日誌,直接升級不是好的習慣,建議關注我們的更新日誌(github 上有專門的文件);
- 我們的版本開發原則:在儘量保證相容的情況下,增加新功能,砍掉少量不合理的功能;
- 我們的單元測試數量:4000+,這是我們引以自豪,釋出版本的保障;
入戲準備
FreeSql 是 .Net ORM,能支援 .NetFramework4.0+、.NetCore、Xamarin、XAUI、Blazor、以及還有說不出來的執行平臺,因為程式碼綠色無依賴,支援新平臺非常簡單。目前單元測試數量:4000+,Nuget下載數量:123K+,原始碼幾乎每天都有提交。值得高興的是 FreeSql 加入了 ncc 開源社群:https://github.com/dotnetcore/FreeSql,加入組織之後社群責任感更大,需要更努力做好品質,為開源社群出一份力。QQ開發群:4336577
為什麼要重複造輪子?
FreeSql 主要優勢在於易用性上,基本是開箱即用,在不同資料庫之間切換相容性比較好。作者花了大量的時間精力在這個專案,肯請您花半小時瞭解下專案,謝謝。
FreeSql 整體的功能特性如下:
- 支援 CodeFirst 對比結構變化遷移;
- 支援 DbFirst 從資料庫匯入實體類;
- 支援 豐富的表示式函式,自定義解析;
- 支援 批量新增、批量更新、BulkCopy;
- 支援 導航屬性,貪婪載入、延時載入、級聯儲存;
- 支援 讀寫分離、分表分庫,租戶設計;
- 支援 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/達夢/MsAccess;
1.0 -> 1.5 更新的重要功能如下:
一、UnitOfWorkManager 工作單元管理器,可實現 Spring 事務設計;
二、IFreeSql.InsertOrUpdate 實現批量儲存,執行時根據資料庫自動適配執行 merge into 或者 on duplicate key update;
三、ISelect.WhereDynamicFilter 方法實現動態過濾條件(與前端互動);
四、自動適配表示式解析 yyyyMMdd 常用 c# 日期格式化;
五、IUpdate.SetSourceIgnore 方法實現忽略屬性值為 null 的欄位;
六、FreeSql.Provider.Dameng 基於 DmProvider Ado.net 訪問達夢資料庫;
七、自動識別 EFCore 常用的實體特性,FreeSql.DbContext 擁有和 EFCore 高相似度的語法,並且支援 90% 相似的 FluentApi;
八、ISelect.ToTreeList 擴充套件方法查詢資料,把配置父子導航屬性的實體加工為樹型 List;
九、BulkCopy 相關方法提升大批量資料插入效能;
十、Sqlite :memrory: 記憶體模式;
FreeSql 使用非常簡單,只需要定義一個 IFreeSql 物件即可:
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.MySql, connectionString)
.UseAutoSyncStructure(true) //自動同步實體結構到資料庫
.Build(); //請務必定義成 Singleton 單例模式
UnitOfWorkManager 工作單元管理器
public class SongService
{
BaseRepository<Song> _repo;
public SongService(BaseRepository<Song> repo)
{
_repo = repo;
}
[Transactional]
public virtual void Test1()
{
_repo.Insert(new Song { Title = "卡農1" }); //事務1
this.Test2();
}
[Transactional(Propagation = Propagation.Nested)] //巢狀事務,新的(不使用 Test1 的事務)
public virtual void Test2()
{
_repo.Insert(new Song { Title = "卡農2" });
}
}
BaseRepository 是 FreeSql.BaseRepository 包實現的通用倉儲類,實際專案中可以繼承它再使用。
Propagation 的模式參考了 Spring 事務,在以下幾種模式:
- Requierd:如果當前沒有事務,就新建一個事務,如果已存在一個事務中,加入到這個事務中,預設的選擇。
- Supports:支援當前事務,如果沒有當前事務,就以非事務方法執行。
- Mandatory:使用當前事務,如果沒有當前事務,就丟擲異常。
- NotSupported:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
- Never:以非事務方式執行操作,如果當前事務存在則丟擲異常。
- Nested:以巢狀事務方式執行。(上面的例子使用的這個)
UnitOfWorkManager 正是幹這件事的。避免了每次對資料操作都要現獲得 Session 例項來啟動事務/提交/回滾事務還有繁瑣的Try/Catch操作。這些也是 AOP(面向切面程式設計)機制很好的應用。一方面使開發業務邏輯更清晰、專業分工更加容易進行。另一方面就是應用 AOP 隔離降低了程式的耦合性使我們可以在不同的應用中將各個切面結合起來使用大大提高了程式碼重用度。
使用前準備第一步:配置 Startup.cs 注入
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IFreeSql>(fsql);
services.AddScoped<UnitOfWorkManager>();
services.AddFreeRepository(null, typeof(Startup).Assembly);
}
UnitOfWorkManager 成員 | 說明 |
---|---|
IUnitOfWork Current | 返回當前的工作單元 |
void Binding(repository) | 將倉儲的事務交給它管理 |
IUnitOfWork Begin(propagation, isolationLevel) | 建立工作單元 |
使用前準備第二步:定義事務特性
[AttributeUsage(AttributeTargets.Method)]
public class TransactionalAttribute : Attribute
{
/// <summary>
/// 事務傳播方式
/// </summary>
public Propagation Propagation { get; set; } = Propagation.Requierd;
/// <summary>
/// 事務隔離級別
/// </summary>
public IsolationLevel? IsolationLevel { get; set; }
}
使用前準備第三步:引入動態代理庫
在 Before 從容器中獲取 UnitOfWorkManager,呼叫它的 var uow = uowManager.Begin(attr.Propagation, attr.IsolationLevel) 方法
在 After 呼叫 Before 中的 uow.Commit 或者 Rollback 方法,最後呼叫 uow.Dispose
自問自答:是不是進方法就開事務呢?
不一定是真實事務,有可能是虛的,就是一個假的 unitofwork(不帶事務),也有可能是延用上一次的事務,也有可能是新開事務,具體要看傳播模式。
IFreeSql.InsertOrUpdate 批量插入或更新
IFreeSql 定義了 InsertOrUpdate 方法實現批量插入或更新的功能,利用的是資料庫特性進行儲存,執行時根據資料庫自動適配:
Database | Features |
---|---|
MySql | on duplicate key update |
PostgreSQL | on conflict do update |
SqlServer | merge into |
Oracle | merge into |
Sqlite | replace into |
Dameng | merge into |
fsql.InsertOrUpdate<T>()
.SetSource(items) //需要操作的資料
.ExecuteAffrows();
由於我們前面定義 fsql 變數的型別是 MySql,所以執行的語句大概是這樣的:
INSERT INTO `T`(`id`, `name`) VALUES(1, '001'), (2, '002'), (3, '003'), (4, '004')
ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`)
當實體類有自增屬性時,批量 InsertOrUpdate 最多可被拆成兩次執行,內部計算出未設定自增值、和有設定自增值的資料,分別執行 insert into 和 上面講到的 merge into 兩種命令(採用事務執行)。
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"" : ""Eq"",
""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%')
支援的操作符:Contains/StartsWith/EndsWith/NotContains/NotStartsWith/NotEndsWith、Equals/Eq/NotEqual、GreaterThan/GreaterThanOrEqual、LessThan/LessThanOrEqual
表示式解析 yyyyMMdd c# 常用日期格式化
不知道大家有沒有這個困擾,在 ORM 表示式使用 DateTime.Now.ToString("yyyyMM") 是件很難轉換的事,在我適配的這些資料庫中,只有 MsAccess 可以直接翻譯成對應的 SQL 執行。
這個想法來自另一個 ORM issues,我時不時會去了解其他 ORM 優點和缺陷,以便給 FreeSql 做補充。
想法出來之後當於,也就是昨天 2020/5/24 奮戰一宿完成的,除了每個資料庫進行編碼適配外,更多的時間耗在了單元測試上,目前已全部通過(4000+單元測試不是吹的)。
僅以此功能讓大家感受一下 FreeSql 的認真,他不是一些人口中所說的個人專案,謝謝。
var dtn = DateTime.Parse("2020-1-1 0:0:0");
var dts = Enumerable.Range(1, 12).Select(a => dtn.AddMonths(a))
.Concat(Enumerable.Range(1, 31).Select(a => dtn.AddDays(a)))
.Concat(Enumerable.Range(1, 24).Select(a => dtn.AddHours(a)))
.Concat(Enumerable.Range(1, 60).Select(a => dtn.AddMinutes(a)))
.Concat(Enumerable.Range(1, 60).Select(a => dtn.AddSeconds(a)));
foreach (var dt in dts)
{
Assert.Equal(dt.ToString("yyyy-MM-dd HH:mm:ss.fff"), fsql.Select<T>().First(a => dt.ToString()));
Assert.Equal(dt.ToString("yyyy-MM-dd HH:mm:ss"), fsql.Select<T>().First(a => dt.ToString("yyyy-MM-dd HH:mm:ss")));
Assert.Equal(dt.ToString("yyyy-MM-dd HH:mm"), fsql.Select<T>().First(a => dt.ToString("yyyy-MM-dd HH:mm")));
Assert.Equal(dt.ToString("yyyy-MM-dd HH"), fsql.Select<T>().First(a => dt.ToString("yyyy-MM-dd HH")));
Assert.Equal(dt.ToString("yyyy-MM-dd"), fsql.Select<T>().First(a => dt.ToString("yyyy-MM-dd")));
Assert.Equal(dt.ToString("yyyy-MM"), fsql.Select<T>().First(a => dt.ToString("yyyy-MM")));
Assert.Equal(dt.ToString("yyyyMMddHHmmss"), fsql.Select<T>().First(a => dt.ToString("yyyyMMddHHmmss")));
Assert.Equal(dt.ToString("yyyyMMddHHmm"), fsql.Select<T>().First(a => dt.ToString("yyyyMMddHHmm")));
Assert.Equal(dt.ToString("yyyyMMddHH"), fsql.Select<T>().First(a => dt.ToString("yyyyMMddHH")));
Assert.Equal(dt.ToString("yyyyMMdd"), fsql.Select<T>().First(a => dt.ToString("yyyyMMdd")));
Assert.Equal(dt.ToString("yyyyMM"), fsql.Select<T>().First(a => dt.ToString("yyyyMM")));
Assert.Equal(dt.ToString("yyyy"), fsql.Select<T>().First(a => dt.ToString("yyyy")));
Assert.Equal(dt.ToString("HH:mm:ss"), fsql.Select<T>().First(a => dt.ToString("HH:mm:ss")));
Assert.Equal(dt.ToString("yyyy MM dd HH mm ss yy M d H hh h"), fsql.Select<T>().First(a => dt.ToString("yyyy MM dd HH mm ss yy M d H hh h")));
Assert.Equal(dt.ToString("yyyy MM dd HH mm ss yy M d H hh h m s tt t").Replace("上午", "AM").Replace("下午", "PM").Replace("上", "A").Replace("下", "P"), fsql.Select<T>().First(a => dt.ToString("yyyy MM dd HH mm ss yy M d H hh h m s tt t")));
}
支援常用 c# 日期格式化,yyyy MM dd HH mm ss yy M d H hh h m s tt t
tt t 為 AM PM
AM PM 這兩個轉換不完美,勉強能使用。
IUpdate.SetSourceIgnore 不更新 null 欄位
這個功能被使用者提了幾次,每一次都認為 FreeSql.Repository 的狀態對比可以完成這件事。
這一次作者心疼他們了,為什麼一定要用某個功能限制住使用者?大家是否經常聽誰說 EF框架、MVC框架,框架的定義其實是約束+規範。
作者不想做這樣的約束,作者更希望儘量提供多一些實用功能讓使用者自己選擇,把專案定義為:功能元件。
fsql.Update<Song>()
.SetSourceIgnore(item, col => col == null)
.ExecuteAffrows();
第二個引數是 Func<object, bool> 型別,col 相當於屬性的值,上面的程式碼更新實體 item 的時候會忽略 == null 的屬性。
Ado.net 訪問達夢資料庫
武漢達夢資料庫有限公司成立於2000年,為中國電子資訊產業集團(CEC)旗下基礎軟體企業,專業從事資料庫管理系統的研發、銷售與服務,同時可為使用者提供大資料平臺架構諮詢、資料技術方案規劃、產品部署與實施等服務。多年來,達夢公司始終堅持原始創新、獨立研發,目前已掌握資料管理與資料分析領域的核心前沿技術,擁有全部原始碼,具有完全自主智慧財產權。
不知道大家沒有聽說過相關政策,政府推動國產化以後是趨勢,雖然 .NET 不是國產,但是目前無法限制程式語言,當下正在對作業系統、資料庫強制推進。
我們知道 EFCore for oracle 問題多,並且現在還沒更新到 3.x,在這樣的背景下,一個國產資料庫更不能指望誰實現好用的 EFCore。目前看來除了 EFCore for sqlserver 我們沒把握完全佔優勢,起碼在其他資料庫肯定是我們更接地氣。
言歸正傳,達夢資料庫其實蠻早就支援了,之前是以 Odbc 的方式實現的,後面根據使用者的反饋 Odbc 環境問題比較麻煩,經研究決定支援 ado.net 適配,讓使用者更加方便。使用 ado.net 方式連線達夢只需要修改 IFreeSql 建立時候的型別即可,如下:
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.Dameng, connectionString)
.UseAutoSyncStructure(true) //自動同步實體結構到資料庫
.Build(); //請務必定義成 Singleton 單例模式
相容 EFCore 實體特性、FluentApi
EFCore 目前使用者量最多,為了方便一些專案過渡到 FreeSql,我們做了一些 “AI”:
- 自動識別 EFCore 實體特性:Key/Required/NotMapped/Table/Column
[Table("table01")] //這個其實是 EFCore 的特性
class MyTable
{
[Key]
public int Id { get; set; }
}
- 與 EFCore 90% 相似的 FluentApi
fsql.CodeFirst.Entity<Song>(eb => {
eb.ToTable("tb_song");
eb.Ignore(a => a.Field1);
eb.Property(a => a.Title).HasColumnType("varchar(50)").IsRequired();
eb.Property(a => a.Url).HasMaxLength(100);
eb.Property(a => a.RowVersion).IsRowVersion();
eb.Property(a => a.CreateTime).HasDefaultValueSql("current_timestamp");
eb.HasKey(a => a.Id);
eb.HasIndex(a => new { a.Id, a.Title }).IsUnique().HasName("idx_xxx11");
//一對多、多對一
eb.HasOne(a => a.Type).HasForeignKey(a => a.TypeId).WithMany(a => a.Songs);
//多對多
eb.HasMany(a => a.Tags).WithMany(a => a.Songs, typeof(Song_tag));
});
fsql.CodeFirst.Entity<SongType>(eb => {
eb.HasMany(a => a.Songs).WithOne(a => a.Type).HasForeignKey(a => a.TypeId);
eb.HasData(new[]
{
new SongType
{
Id = 1,
Name = "流行",
Songs = new List<Song>(new[]
{
new Song{ Title = "真的愛你" },
new Song{ Title = "愛你一萬年" },
})
},
new SongType
{
Id = 2,
Name = "鄉村",
Songs = new List<Song>(new[]
{
new Song{ Title = "鄉里鄉親" },
})
},
});
});
public class SongType {
public int Id { get; set; }
public string Name { get; set; }
public List<Song> Songs { get; set; }
}
public 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; }
public int TypeId { get; set; }
public SongType Type { get; set; }
public int Field1 { get; set; }
public long RowVersion { get; set; }
}
ISelect.ToTreeList 查詢樹型資料 List
這是幾個意思?有做過父子關係的表應該知道的,把資料查回來了是平面的,需要再用遞迴轉化為樹型。考慮到這個功能實用性比較高,所以就整合了進來。來自單元測試的一段程式碼:
var repo = fsql.GetRepository<VM_District_Child>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = true;
repo.DbContextOptions.NoneParameter = true;
repo.Insert(new VM_District_Child
{
Code = "100000",
Name = "中國",
Childs = new List<VM_District_Child>(new[] {
new VM_District_Child
{
Code = "110000",
Name = "北京市",
Childs = new List<VM_District_Child>(new[] {
new VM_District_Child{ Code="110100", Name = "北京市" },
new VM_District_Child{ Code="110101", Name = "東城區" },
})
}
})
});
var t3 = fsql.Select<VM_District_Child>().ToTreeList();
Assert.Single(t3);
Assert.Equal("100000", t3[0].Code);
Assert.Single(t3[0].Childs);
Assert.Equal("110000", t3[0].Childs[0].Code);
Assert.Equal(2, t3[0].Childs[0].Childs.Count);
Assert.Equal("110100", t3[0].Childs[0].Childs[0].Code);
Assert.Equal("110101", t3[0].Childs[0].Childs[1].Code);
注意:實體需要配置父子導航屬性
BulkCopy 大批量資料
原先 FreeSql 對批量資料操作就做得還可以,例如批量資料超過資料庫某些限制的,會拆分執行,效能其實也還行。
本需求也是來自使用者,然後就實現了,實現完了我還專門做了效能測試對比,sqlserver bulkcopy 收益比較大,mysql 收益非常小。
測試結果(52個欄位,18W-50行資料,單位ms):
18W | 1W | 5K | 500 | 50 | |
---|---|---|---|---|---|
MySql 5.5 ExecuteAffrows | 38,481 | 2,234 | 1,136 | 167 | 30 |
MySql 5.5 ExecuteMySqlBulkCopy | 28,405 | 1,142 | 657 | 592 | 22 |
SqlServer Express ExecuteAffrows | 402,355 | 24,847 | 11,465 | 915 | 88 |
SqlServer Express ExecuteSqlBulkCopy | 21,065 | 578 | 326 | 79 | 48 |
PostgreSQL 10 ExecuteAffrows | 46,756 | 3,294 | 2,269 | 209 | 37 |
PostgreSQL 10 ExecutePgCopy | 10,090 | 583 | 337 | 61 | 25 |
Oracle XE ExecuteAffrows | - | - | - | 10,648 | 200 |
Sqlite ExecuteAffrows | 28,554 | 1,149 | 701 | 91 | 35 |
Oracle 插入效能不用懷疑,可能安裝學生版限制較大
測試結果(10個欄位,18W-50行資料,單位ms):
18W | 1W | 5K | 500 | 50 | |
---|---|---|---|---|---|
MySql 5.5 ExecuteAffrows | 11,171 | 866 | 366 | 50 | 34 |
MySql 5.5 ExecuteMySqlBulkCopy | 6,504 | 399 | 257 | 100 | 16 |
SqlServer Express ExecuteAffrows | 47,204 | 2,275 | 1,108 | 123 | 16 |
SqlServer Express ExecuteSqlBulkCopy | 4,248 | 127 | 71 | 14 | 10 |
PostgreSQL 10 ExecuteAffrows | 9,786 | 568 | 336 | 34 | 6 |
PostgreSQL 10 ExecutePgCopy | 4,081 | 167 | 93 | 12 | 2 |
Oracle XE ExecuteAffrows | - | - | - | 731 | 33 |
Sqlite ExecuteAffrows | 4,524 | 246 | 137 | 19 | 11 |
測試結果,是在相同作業系統下進行的,並且都有預熱
ExecuteMySqlBulkCopy 方法在 FreeSql.Provider.MySqlConnector 中實現的
Sqlite :memory: 記憶體模式
瞭解 EFCore 應該知道有一個 inMemory 實現,Sqlite 其實也有記憶體模式,所以在非常棒(忍不住)的 FreeSql.Provider.Sqlite 稍加適配就可以實現 inMemory 模式了。
使用 inMemory 模式非常簡單,只需要修改 IFreeSql 建立的型別,以及連線字串即可:
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.Sqlite, "Data Source=:memory:")
.UseAutoSyncStructure(true) //自動同步實體結構到資料庫
.Build(); //請務必定義成 Singleton 單例模式
記憶體模式 + FreeSql CodeFirst 功能,用起來體驗還是不錯的。因為每次都要遷移結構,fsql 釋放資料就沒了。
終於寫完了
終於寫完了,這篇文章是我有史以來編輯最長時間的,歷時 4小時!!!原本我可以利用這 4小時編寫一堆膠水程式碼,卻非要寫推廣的文章,真心希望正在使用的、善良的您能動一動小手指,把文章轉發一下,讓更多人知道 .NET 有這樣一個好用的 ORM 存在。謝謝了!!
FreeSql 開源協議 MIT https://github.com/dotnetcore/FreeSql,可以商用,文件齊全。QQ開發群:4336577
CSRedisCore 說:FreeSql 的待遇也好太多了。
如果你有好的 ORM 實現想法,歡迎給作者留言討論,謝謝觀看!