寫在開頭
2018年11月的某一天,頭腦發熱開啟了 FreeSql 開源專案之旅,時間一晃已經四年多,當初從舒服區走向一個巨大的坑,回頭一看後背一涼。四年時間從無到有,經歷了數不清的日夜奮戰(有人問我花了多長時間投入,答案:全職x2 + 前兩年無休息,以及後面兩年的持續投入)。今天 FreeSql 已經很強大,感謝第一期、第二期、第N期持續提出建議的網友。
FreeSql 現如今已經是一個穩定的版本,主要體現:
- API 已經確定,不會輕易推翻重作調整,堅持十年不變的原則,讓使用者真真正正的不再關心 ORM 使用問題;
- 單元測試覆蓋面廣,6336+ 個單元測試,小版本更新升級無須考慮修東牆、補西牆的問題;
- 經歷四年時間的生產考驗,nuget下載量已超過900K+,平均每日750+;
感嘆:有些人說 .Net 陷入 orm 怪圈,動手的沒幾個,指點江山的一堆,.Net orm 真的如他們所講的簡單嗎?
專案介紹
FreeSql 是 .Net ORM,能支援 .NetFramework4.0+、.NetCore、Xamarin、MAUI、Blazor、以及還有說不出來的執行平臺,因為程式碼綠色無依賴,支援新平臺非常簡單。目前單元測試數量:6336+,Nuget下載數量:900K+。QQ群:4336577(已滿)、8578575(線上)、52508226(線上)
溫馨提醒:以下內容無商吹成份,FreeSql 不打誑語
為什麼要重複造輪子?
FreeSql 主要優勢在於易用性上,基本是開箱即用,在不同資料庫之間切換相容性比較好。作者花了大量的時間精力在這個專案,肯請您花半小時瞭解下專案,謝謝。FreeSql 整體的功能特性如下:
- 支援 CodeFirst 對比結構變化遷移;
- 支援 DbFirst 從資料庫匯入實體類;
- 支援 豐富的表示式函式,自定義解析;
- 支援 批量新增、批量更新、BulkCopy;
- 支援 導航屬性,貪婪載入、延時載入、級聯儲存;
- 支援 讀寫分離、分表分庫,租戶設計;
- 支援 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/達夢/神通/人大金倉/翰高/MsAccess Ado.net 實現包,以及 Odbc 的專門實現包;
5500+個單元測試作為基調,支援10多數資料庫,我們提供了通用Odbc理論上支援所有資料庫,目前已知有群友使用 FreeSql 操作華為高斯、mycat、tidb 等資料庫。安裝時只需要選擇對應的資料庫實現包:
dotnet add packages FreeSql.Provider.MySql
FreeSql.Repository 是 FreeSql 專案的延申擴充套件類庫,支援 .NETFramework4.0+、.NETCore2.0+、.NET5+、Xamarin 平臺。
FreeSql.Repository 除了 CRUD 還有很多實用性功能,不防耐下心花10分鐘看完。
01 安裝
環境1:.NET Core 或 .NET 5.0+
dotnet add package FreeSql.Repository
環境2、.NET Framework
Install-Package FreeSql.DbContext
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.Sqlite, connectionString)
.UseAutoSyncStructure(true) //自動遷移實體的結構到資料庫
.UseNoneCommandParameter(true)
.UseMonitorCommand(cmd => Console.WriteLine(cmd.CommandText))
.Build(); //請務必定義成 Singleton 單例模式
02 使用方法
方法1、IFreeSql 的擴充套件方法;
var curd = fsql.GetRepository<Topic>();
注意:Repository 物件多執行緒不安全,因此不應在多個執行緒上同時對其執行工作。
- fsql.GetRepository 方法返回新倉儲例項
- 不支援從不同的執行緒同時使用同一倉儲例項
以下為了方便測試程式碼演示,我們都使用方法1,fsql.GetRepository
建立新倉儲例項
方法2、繼承實現;
public class TopicRepository: BaseRepository<Topic, int> {
public TopicRepository(IFreeSql fsql) : base(fsql, null, null) {}
//在這裡增加 CURD 以外的方法
}
方法3、依賴注入;
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IFreeSql>(Fsql);
services.AddFreeRepository(null, this.GetType().Assembly);
}
//在控制器使用
public TopicController(IBaseRepository<Topic> repo) {
}
03 新增資料
repo.Insert 插入資料,適配了各資料庫優化執行 ExecuteAffrows/ExecuteIdentity/ExecuteInserted
1、如果表有自增列,插入資料後應該要返回 id。
var repo = fsql.GetRepository<Topic>();
repo.Insert(topic);
內部會將插入後的自增值填充給 topic.Id
2、批量插入
var repo = fsql.GetRepository<Topic>();
var topics = new [] { new Topic { ... }, new Topic { ... } };
repo.Insert(topics);
3、插入資料庫時間
使用 [Column(ServerTime = DateTimeKind.Utc)] 特性,插入資料時,使用適配好的每種資料庫內容,如 getutcdate()
4、插入特殊型別
使用 [Column(RereadSql = "{0}.STAsText()", RewriteSql = "geography::STGeomFromText({0},4236)")] 特性,插入和讀取時特別處理
5、插入時忽略
使用 [Column(CanInsert = false)] 特性
04 更新資料
1、只更新變化的屬性
var repo = fsql.GetRepository<Topic>();
var item = repo.Where(a => a.Id == 1).First(); //此時快照 item
item.Name = "newtitle";
repo.Update(item); //對比快照時的變化
//UPDATE `Topic` SET `Title` = 'newtitle'
//WHERE (`Id` = 1)
2、手工管理狀態
var repo = fsql.GetRepository<Topic>();
var item = new Topic { Id = 1 };
repo.Attach(item); //此時快照 item
item.Title = "newtitle";
repo.Update(item); //對比快照時的變化
//UPDATE `Topic` SET `Title` = 'newtitle'
//WHERE (`Id` = 1)
3、直接使用 repo.UpdateDiy,它是 IFreeSql 提供的原生 IUpdate 物件,功能更豐富
05 級聯儲存資料
實踐發現,N對1 不適合做級聯儲存。儲存 Topic 的時候把 Type 資訊也儲存?我個人認為自下向上儲存的功能太不可控了,FreeSql 目前不支援自下向上儲存。因此下面我們只講 OneToOne/OneToMany/ManyToMany 級聯儲存。至於 ManyToOne 級聯儲存使用手工處理,更加安全可控。
功能1:SaveMany 手工儲存
完整儲存,對比表已存在的資料,計算出新增、修改、刪除執行。
遞迴儲存導航屬性不安全,不可控,並非技術問題,而是出於安全考慮,提供了手工完整儲存的方式。
var repo = fsql.GetRepository<Type>();
var type = new Type
{
name = "c#",
Topics = new List<Topic>(new[]
{
new Topic { ... }
})
};
repo.Insert(type);
repo.SaveMany(type, "Topics"); //手工完整儲存 Topics
- SaveMany 僅支援 OneToMany、ManyToMany 導航屬性
- 只儲存 Topics,不向下遞迴追朔
- 當 Topics 為 Empty 時,刪除 type 存在的 Topics 所有表資料,確認?
- ManyToMany 機制為,完整對比儲存中間表,外部表只追加不更新
如:
- 本表 Topic
- 外部表 Tag
- 中間表 TopicTag
功能2:EnableCascadeSave 倉儲級聯儲存
DbContext/Repository EnableCascadeSave 可實現儲存物件的時候,遞迴追朔其 OneToOne/OneToMany/ManyToMany 導航屬性也一併儲存,本文件說明機制防止誤用。
1、OneToOne 級聯儲存
v3.2.606+ 支援,並且支援級聯刪除功能(文件請向下瀏覽)
2、OneToMany 追加或更新子表,不刪除子表已存在的資料
var repo = fsql.GetRepository<Type>();
repo.DbContextOptions.EnableCascadeSave = true; //需要手工開啟
repo.Insert(type);
- 不刪除 Topics 子表已存在的資料,確認?
- 當 Topics 屬性為 Empty 時,不做任何操作,確認?
- 儲存 Topics 的時候,還會儲存 Topics[0-..] 的下級集合屬性,向下18層,確認?
向下18層的意思,比如【型別】表,下面有集合屬性【文章】,【文章】下面有集合屬性【評論】。
儲存【型別】表物件的時候,他會向下檢索出集合屬性【文章】,然後如果【文章】被儲存的時候,再繼續向下檢索出集合屬性【評論】。一起做 InsertOrUpdate 操作。
3、ManyToMany 完整對比儲存中間表,追加外部表
完整對比儲存中間表,對比【多對多】中間表已存在的資料,計算出新增、修改、刪除執行。
追加外部表,只追加不更新。
- 本表 Topic
- 外部表 Tag
- 中間表 TopicTag
06 刪除資料
var repo = fsql.GetRepository<Topic>();
repo.Delete(new Topic { Id = 1 }); //有過載方法 repo.Delete(Topic[])
var repo2 = fsql.GetRepository<Topic, int>(); //int 是主鍵型別,相比 repo 物件多了 Delete(int) 方法
repo2.Delete(1);
07 級聯刪除資料
第一種:基於【物件】級聯刪除
比如使用過 Include/IncludeMany 查詢的物件,可以使用此方法級聯刪除它們。
var repo = fsql.GetRepository<Group>();
repo.DbContextOptions.EnableCascadeSave = true; //關鍵設定
repo.Insert(new UserGroup
{
GroupName = "group01",
Users = new List<User>
{
new User { Username = "admin01", Password = "pwd01", UserExt = new UserExt { Remark = "使用者備註01" } },
new User { Username = "admin02", Password = "pwd02", UserExt = new UserExt { Remark = "使用者備註02" } },
new User { Username = "admin03", Password = "pwd03", UserExt = new UserExt { Remark = "使用者備註03" } },
}
}); //級聯新增測試資料
//INSERT INTO "usergroup"("groupname") VALUES('group01') RETURNING "id"
//INSERT INTO "user"("username", "password", "groupid") VALUES('admin01', 'pwd01', 1), ('admin02', 'pwd02', 1), ('admin03', 'pwd03', 1) RETURNING "id" as "Id", "username" as "Username", "password" as "Password", "groupid" as "GroupId"
//INSERT INTO "userext"("userid", "remark") VALUES(3, '使用者備註01'), (4, '使用者備註02'), (5, '使用者備註03')
var groups = repo.Select
.IncludeMany(a => a.Users,
then => then.Include(b => b.UserExt))
.ToList();
repo.Delete(groups); //級聯刪除,遞迴向下遍歷 group OneToOne/OneToMany/ManyToMany 導航屬性
//DELETE FROM "userext" WHERE ("userid" IN (3,4,5))
//DELETE FROM "user" WHERE ("id" IN (3,4,5))
//DELETE FROM "usergroup" WHERE ("id" = 1)
第二種:基於【資料庫】級聯刪除,不依賴資料庫外來鍵
根據設定的導航屬性,遞迴刪除 OneToOne/OneToMany/ManyToMany 對應資料,並返回已刪除的資料。此功能不依賴資料庫外來鍵
var repo = fsql.GetRepository<Group>();
var ret = repo.DeleteCascadeByDatabase(a => a.Id == 1);
//SELECT a."id", a."username", a."password", a."groupid" FROM "user" a WHERE (a."groupid" = 1)
//SELECT a."userid", a."remark" FROM "userext" a WHERE (a."userid" IN (3,4,5))
//DELETE FROM "userext" WHERE ("userid" IN (3,4,5))
//DELETE FROM "user" WHERE ("id" IN (3,4,5))
//DELETE FROM "usergroup" WHERE ("id" = 1)
//ret Count = 7 System.Collections.Generic.List<object>
// [0] {UserExt} object {UserExt}
// [1] {UserExt} object {UserExt}
// [2] {UserExt} object {UserExt}
// [3] {User} object {User}
// [4] {User} object {User}
// [5] {User} object {User}
// [6] {UserGroup} object {UserGroup}
public class Group
{
[Column(IsIdentity = true)]
public int Id { get; set; }
public string GroupName { get; set; }
[Navigate(nameof(User.GroupId))]
public List<User> Users { get; set; }
}
public class User
{
[Column(IsIdentity = true)]
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public int GroupId { get; set; }
[Navigate(nameof(Id))]
public UserExt UserExt { get; set; }
}
public class UserExt
{
[Column(IsPrimary = true)]
public int UserId { get; set; }
public string Remark { get; set; }
[Navigate(nameof(UserId))]
public User User { get; set; }
}
08 新增或修改資料
var repo = fsql.GetRepository<Topic>();
repo.InsertOrUpdate(item);
如果內部的狀態管理存在資料,則更新。
如果內部的狀態管理不存在資料,則查詢資料庫,判斷是否存在。
存在則更新,不存在則插入
缺點:不支援批量操作
提醒:IFreeSql 也定義了 InsertOrUpdate 方法,兩者實現機制不同,它利用了資料庫特性:
Database | Features | Database | Features | |
---|---|---|---|---|
MySql | on duplicate key update | 達夢 | merge into | |
PostgreSQL | on conflict do update | 人大金倉 | on conflict do update | |
SqlServer | merge into | 神通 | merge into | |
Oracle | merge into | 南大通用 | merge into | |
Sqlite | replace into | MsAccess | 不支援 | |
Firebird | merge into |
fsql.InsertOrUpdate<T>()
.SetSource(items) //需要操作的資料
//.IfExistsDoNothing() //如果資料存在,啥事也不幹(相當於只有不存在資料時才插入)
.ExecuteAffrows();
09 批量編輯資料
var repo = fsql.GetRepository<BeginEdit01>();
var cts = new[] {
new BeginEdit01 { Name = "分類1" },
new BeginEdit01 { Name = "分類1_1" },
new BeginEdit01 { Name = "分類1_2" },
new BeginEdit01 { Name = "分類1_3" },
new BeginEdit01 { Name = "分類2" },
new BeginEdit01 { Name = "分類2_1" },
new BeginEdit01 { Name = "分類2_2" }
}.ToList();
repo.Insert(cts);
repo.BeginEdit(cts); //開始對 cts 進行編輯
cts.Add(new BeginEdit01 { Name = "分類2_3" });
cts[0].Name = "123123";
cts.RemoveAt(1);
var affrows = repo.EndEdit(); //完成編輯
Assert.Equal(3, affrows);
class BeginEdit01
{
public Guid Id { get; set; }
public string Name { get; set; }
}
上面的程式碼 EndEdit 方法執行的時候產生 3 條 SQL 如下:
INSERT INTO "BeginEdit01"("Id", "Name") VALUES('5f26bf07-6ac3-cbe8-00da-7dd74818c3a6', '分類2_3')
UPDATE "BeginEdit01" SET "Name" = '123123'
WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd01be76e26')
DELETE FROM "BeginEdit01" WHERE ("Id" = '5f26bf00-6ac3-cbe8-00da-7dd11bcf54dc')
場景:winform 載入表資料後,一頓新增、修改、刪除操作之後,點選【儲存】
提醒:該操作只對變數 cts 有效,不是針對全表對比更新。
10 弱型別 CRUD
var repo = fsql.GetRepository<object>();
repo.AsType(typeof(Topic));
var item = (object)new Topic { Title = "object title" };
repo.Insert(item);
11 無引數化命令
支援引數化、無引數化命令執行,有一些特定的資料庫,使用無引數化命令執行效率更高哦,並且除錯起來更直觀。
var repo = fsql.GetRepository<object>();
repo.DbContextOptions.NoneParameter = true;
12 工作單元(事務)
UnitOfWork 可將多個倉儲放在一個單元管理執行,最終通用 Commit 執行所有操作,內部採用了資料庫事務。
方法1:隨時建立使用
using (var uow = fsql.CreateUnitOfWork())
{
var typeRepo = fsql.GetRepository<Type>();
var topicRepo = fsql.GetRepository<Topic>();
typeRepo.UnitOfWork = uow;
topicRepo.UnitOfWork = uow;
typeRepo.Insert(new Type());
topicRepo.Insert(new Topic());
uow.Orm.Insert(new Topic()).ExecuteAffrows();
//uow.Orm 和 fsql 都是 IFreeSql
//uow.Orm CRUD 與 uow 是一個事務
//fsql CRUD 與 uow 不在一個事務
uow.Commit();
}
方法2:使用 AOP + UnitOfWorkManager 實現多種事務傳播
本段內容引導,如何在 asp.net core 專案中使用特性(註解) 的方式管理事務。
UnitOfWorkManager 支援六種傳播方式(propagation),意味著跨方法的事務非常方便,並且支援同步非同步:
- Requierd:如果當前沒有事務,就新建一個事務,如果已存在一個事務中,加入到這個事務中,預設的選擇。
- Supports:支援當前事務,如果沒有當前事務,就以非事務方法執行。
- Mandatory:使用當前事務,如果沒有當前事務,就丟擲異常。
- NotSupported:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
- Never:以非事務方式執行操作,如果當前事務存在則丟擲異常。
- Nested:以巢狀事務方式執行。
第一步:配置 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 = Begin(attr.Propagation, attr.IsolationLevel) 方法
在 After 呼叫 Before 中的 uow.Commit 或者 Rollback 方法,最後呼叫 uow.Dispose
提醒:動態代理,一定注意處理好非同步 await,否則會出現事務異常的問題
第四步:在 Controller 或者 Service 中使用事務特性
public class TopicService
{
IBaseRepository<Topic> _repoTopic;
IBaseRepository<Detail> _repoDetail;
public TopicService(IBaseRepository<Topic> repoTopic, IBaseRepository<Detail> repoDetail)
{
_repoTopic = repoTopic;
_repoDetail = repoDetail;
}
[Transactional]
public virtual void Test1()
{
//這裡 _repoTopic、_repoDetail 所有 CRUD 操作都是一個工作單元
this.Test2();
}
[Transactional(Propagation = Propagation.Nested)]
public virtual void Test2() //巢狀事務,新的(不使用 Test1 的事務)
{
//這裡 _repoTopic、_repoDetail 所有 CRUD 操作都是一個工作單元
}
}
是不是進方法就開事務呢?
不一定是真實事務,有可能是虛的,就是一個假的 unitofwork(不帶事務)
也有可能是延用上一次的事務
也有可能是新開事務,具體要看傳播模式
示例專案:https://github.com/dotnetcore/FreeSql/tree/master/Examples/aspnetcore_transaction
Autofac 動態代理參考專案:
- https://github.com/luoyunchong/lin-cms-dotnetcore
- https://github.com/luoyunchong/dotnetcore-examples/tree/master/ORM/FreeSql/OvOv.FreeSql.AutoFac.DynamicProxy
- AOP + FreeSql 跨方法非同步事務 https://www.cnblogs.com/igeekfan/p/aop-freesql-autofac.html
13 手工分表
FreeSql 原生用法、FreeSql.Repository 倉儲用法 都提供了 AsTable 方法對分表進行 CRUD 操作,例如:
var repo = fsql.GetRepository<Log>();
repo.AsTable(oldname => $"{oldname}_201903"); //對 Log_201903 表 CRUD
repo.Insert(new Log { ... });
跨庫,但是在同一個資料庫伺服器下,也可以使用 AsTable(oldname => $"db2.dbo.{oldname}")
//跨表查詢
var sql = fsql.Select<User>()
.AsTable((type, oldname) => "table_1")
.AsTable((type, oldname) => "table_2")
.ToSql(a => a.Id);
//select * from (SELECT a."Id" as1 FROM "table_1" a) ftb
//UNION ALL
//select * from (SELECT a."Id" as1 FROM "table_2" a) ftb
分表總結:
- 分表、相同伺服器跨庫 可以使用 AsTable 進行 CRUD;
- AsTable CodeFirst 會自動建立不存在的分表;
- 不可在分表分庫的實體型別中使用《延時載入》;
v3.2.500 按時間自動分表方案:https://github.com/dotnetcore/FreeSql/discussions/1066
寫在最後
FreeSql 他是免費自由的 ORM,也可以說是寶藏 ORM。更多文件請前往 github wiki 檢視。
FreeSql 已經步入第四個年頭,期待少年的你十年後還能歸來在此貼回覆,兌現當初吹過十年不變的承諾。
多的不說了,希望民間的開源力量越來越強大。
希望作者的努力能打動到你,請求正在使用的、善良的您能動一動小手指,把文章轉發一下,讓更多人知道 .NET 有這樣一個好用的 ORM 存在。謝謝!!
FreeSql 使用最寬鬆的開源協議 MIT https://github.com/dotnetcore/FreeSql ,完全可以商用,文件齊全,甚至拿去賣錢也可以。QQ群:4336577(已滿)、8578575(線上)、52508226(線上)