[開源] .Net orm FreeSql 1.5.0 最新版本(番號:很久不見)

廢話開頭

這篇文章是我有史以來編輯最長時間的,歷時 4小時!!!本來我能夠利用這 4小時編寫一堆膠水代碼,真心但願善良的您點個贊,謝謝了!!前端

好久好久沒有寫文章了,上一次仍是在元旦發佈 1.0 版本的時候,今年版本規劃是每個月底發佈小版本(年末發佈 2.0),整年的開源工做主要是收集用戶需求增長功能,完善測試,修復 bug。FreeSql 1.0 -> 1.5 相隔半年有哪些新功能?只能說每一個功能都能讓我興奮,而且能感覺到使用者也同樣興奮(妄想症)。mysql

火燒眉毛的人會問,這更新速度也太快了吧,升級會不會有問題?git

  • 不瞭解版本的更新日誌,直接升級不是好的習慣,建議關注咱們的更新日誌(github 上有專門的文檔);
  • 咱們的版本開發原則:在儘可能保證兼容的狀況下,增長新功能,砍掉少許不合理的功能;
  • 咱們的單元測試數量:4000+,這是咱們引以自豪,發佈版本的保障;

入戲準備

FreeSql 是 .Net ORM,能支持 .NetFramework4.0+、.NetCore、Xamarin、XAUI、Blazor、以及還有說不出來的運行平臺,由於代碼綠色無依賴,支持新平臺很是簡單。目前單元測試數量:4000+,Nuget下載數量:123K+,源碼幾乎天天都有提交。值得高興的是 FreeSql 加入了 ncc 開源社區:https://github.com/dotnetcore/FreeSql,加入組織以後社區責任感更大,須要更努力作好品質,爲開源社區出一份力。QQ開發羣:4336577github

爲何要重複造輪子?sql

FreeSql 主要優點在於易用性上,基本是開箱即用,在不一樣數據庫之間切換兼容性比較好。做者花了大量的時間精力在這個項目,肯請您花半小時瞭解下項目,謝謝。數據庫

FreeSql 總體的功能特性以下:編程

  • 支持 CodeFirst 對比結構變化遷移;
  • 支持 DbFirst 從數據庫導入實體類;
  • 支持 豐富的表達式函數,自定義解析;
  • 支持 批量添加、批量更新、BulkCopy;
  • 支持 導航屬性,貪婪加載、延時加載、級聯保存;
  • 支持 讀寫分離、分表分庫,租戶設計;
  • 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/達夢/MsAccess;

1.0 -> 1.5 更新的重要功能以下:json

1、UnitOfWorkManager 工做單元管理器,可實現 Spring 事務設計;c#

2、IFreeSql.InsertOrUpdate 實現批量保存,執行時根據數據庫自動適配執行 merge into 或者 on duplicate key update;後端

3、ISelect.WhereDynamicFilter 方法實現動態過濾條件(與前端交互);

4、自動適配表達式解析 yyyyMMdd 經常使用 c# 日期格式化;

5、IUpdate.SetSourceIgnore 方法實現忽略屬性值爲 null 的字段;

6、FreeSql.Provider.Dameng 基於 DmProvider Ado.net 訪問達夢數據庫;

7、自動識別 EFCore 經常使用的實體特性,FreeSql.DbContext 擁有和 EFCore 高類似度的語法,而且支持 90% 類似的 FluentApi;

8、ISelect.ToTreeList 擴展方法查詢數據,把配置父子導航屬性的實體加工爲樹型 List;

9、BulkCopy 相關方法提高大批量數據插入性能;

10、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 實現想法,歡迎給做者留言討論,謝謝觀看!

相關文章
相關標籤/搜索