FreeSql 發展到現在,已經有兩種穩定的開發模式,以下先簡單帶過一下。後面才是本文的主題。
方法一:基于 helper 的方式,祼用;
dotnet add package FreeSql
提供 CodeFirst、DbFirst、豐富的表達式樹、讀寫分離、AOP等功能支援;
方法二:基于 Repository + UnitOfWok 的方式;
dotnet add package FreeSql.Repository
這是一個擴充包,提供标準的 IRepository 接口定義與預設實作,以及 UnitOfWork 工作單元的支援,更可怕的是內建了局部/全局過濾器,實作租戶、軟删除等功能不在話下。
不相信嗎?請看以下代碼:
public IServiceProvider ConfigureServices(IServiceCollection services) {
services.AddSingleton<IFreeSql>(fsql);
services.AddMvc();
var builder = new ContainerBuilder();
builder.RegisterFreeRepository(filter => filter
.Apply<ISoftDelete>("SoftDelete", a => a.IsDeleted == false)
.Apply<ITenant>("Tenant", a => a.TenantId == 1)
);
builder.Populate(services);
var container = builder.Build();
return new AutofacServiceProvider(container);
}
比 abpvnext 還要友善,因為 abp 的相關實體需要實作接口 ISoftDelete、ITenant;
我們沒有這個限制,隻要過濾器的表達式解析成功,就算可用;
使用在任何實體上的時候,隻要 [實體].IsDeleted == false 能解析能過,就算可用;
方式三:基于 DbContext
這個項目仍然是一個擴充包,提類似 EFCore 那樣的開發習慣。目前定義的規則如下:
文字規則略顯複雜,後邊有代碼示範,以及圖文介紹在 sqlite 和 sqlserver 下的測試過程。
DbContext
- 提供 SaveChanges 方法;
- 執行隊列;
DbSet
- 提供 Add、AddRange、Remove、RemoveRange、Update、UpdateRange 方法;
- 以及 Select 屬性(連去原有的 FreeSql 查詢對象);
- 私有對象 states,存儲實體的副本哈希集合,key=實體的主鍵值,value=實體;
Add/AddRange(entitys)
- 驗證 entitys 主鍵值,是否存在于 states 中,存在時報錯;
- 驗證 entitys 主鍵中存在自增:
- 若有,則立即開啟 DbContext 事務,按資料庫種類執行相應的方法,最終将傳回的自增值,賦給entitys的屬性;
- 若無,并且 entitys 無主鍵值,則報錯;
- 否則,進入【打包執行隊列】;
- 完成時更新 states;
Remove/RemoveRange(entitys)
- 驗證 entitys 主鍵值,若無則報錯;
- 驗證 states 中是否存在,若無則提醒應該先查詢,再删除;
- 删除 states 對應的實體;
- 清除 entitys 内的自增屬性值、Guid 類型的值,那這個 entitys 将變為可 Add 狀态;
- 進入【打包執行隊列】;
Update/UpdateRange(entitys)
Select
- 立即執行隊列中的指令(打包方式),以免髒讀到未送出的資料;
- 查詢完成時,更新 states 的值;
更新資料規則
- 對比 states 中存在的曆史快照值,傳回即将修改的 fields;
示範代碼
using FreeSql;
public class SongContext : DbContext {
public DbSet<Song> Songs { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder builder) {
builder.UseFreeSql(這裡是IFreeeSql對象);
}
}
public class Song {
[Column(IsIdentity = true)]
public int Id { get; set; }
public DateTime? Create_time { get; set; }
public bool? Is_deleted { get; set; }
public string Title { get; set; }
public string Url { get; set; }
}
public class Tag {
[Column(IsIdentity = true)]
public int Id { get; set; }
public string Name { get; set; }
}
using (var ctx = new SongContext()) {
ctx.Songs.Select.Where(a => a.Id > 10).ToList();
//查詢結果,存入 states
var song = new Song { };
//可插入的 song
ctx.Songs.Add(song);
id = song.Id;
//因有自增類型,立即開啟事務執行SQL,傳回自增值
var adds = Enumerable.Range(0, 100)
.Select(a => new Song { Create_time = DateTime.Now, Is_deleted = false, Title = "xxxx" + a, Url = "url222" })
.ToList();
//建立一堆無主鍵值的資料
ctx.Songs.AddRange(adds);
//立即執行,将自增值賦給 adds 所有元素,因為有自增類型,如果其他類型,指定傳入主鍵值,不會立即執行
for (var a = 0; a < adds.Count; a++)
adds[a].Title = "dkdkdkdk" + a;
ctx.Songs.UpdateRange(adds);
//批量修改,進入隊列
ctx.Songs.RemoveRange(adds.Skip(10).Take(20).ToList());
//批量删除,進入隊列,完成時 10-20 元素的主鍵值會被清除
//ctx.Songs.Update(adds.First());
adds.Last().Url = "skldfjlksdjglkjjcccc";
ctx.Songs.Update(adds.Last());
//單條修改 urls 的值,進入隊列
//throw new Exception("復原");
//ctx.Songs.Select.First();
//這裡做一個查詢,會立即打包【執行隊列】,避免沒有送出的資料,影響查詢結果
ctx.SaveChanges();
//打包【執行隊列】,送出事務
}
在 sqlite 測試

打個岔:為什麼一條條的執行?
- 有自增屬性需要擷取值;
- sqlite 沒有批量插入擷取多個自增的辦法,或者您有招來支一支(萬分感謝);
- 後面采用 sqlserver 測試,就不是這個境況了,insert into values(),(),(),然後利用 output 特性傳回所有值;
- 比較蛋疼的是,這個特性不是所有資料庫都有
可以看見,最終 SaveChanges 時将不會産生影響的指令,一起打包執行,即采用優化合并的方式進行執行。
例如:
ctx.Songs.Update(adds[0]);
ctx.Songs.Update(adds[1]);
這兩個更新操作,會合成一條 SQL 指令執行。
在 sqlserver 測試
其實大緻與 sqlite 下相同,唯一的差別在于 AddRange 的處理方式,如圖:
當插入單條時,采用了第一行代碼的 SQL 指令;
當批量插入時,采用了後面看上去複雜的 SQL 指令;
所有傳入的實體屬性值在執行完成後,都會更新;
特别說明
FreeSql.DbContext 目前仍處于研究開發階段,不适合商用;
總結
為什麼寫這篇文章,時常看見有人說某某 orm 不是真正的 orm,沒有 OO 思想。
希望 FreeSql.DbContext 随着時間的積累,穩定性和成熟度有所提升,不久成為一個真正的 ORM。
有人會擔心,我們第三方做的不靠譜,沒有 EFCore 穩定的說話,這個是當然。
但是我們也有自己的特點,不是嗎?我們可以做到多種資料庫使用習慣的一緻性,這點 EFCore 目前是沒有辦法解決的難題。
從細節出發,我們的口号是:做 .NETCore 最友善的 ORM!
github: https://github.com/2881099/FreeSql 377星
還請獻上寶貴的一星,謝謝觀看!!