1. 程式人生 > >FreeSql v0.11 幾個實用功能說明

FreeSql v0.11 幾個實用功能說明

FreeSql 開源釋出快一年了,立志成為 .Net 平臺方便好用的 ORM,倉庫地址:https://github.com/2881099/FreeSql

隨著不斷的迭代更新,越來越穩定,也越來越強大。預計在一週年的時候(2020年1月1日)釋出 1.0 正式版本。

金九銀十的日子過去了,在這個銅一般的月份裡,鄙人做了幾個重大功能,希望對使用者開發提供更大的便利。

  • 一、Dto 對映查詢
  • 二、IncludeMany 聯級載入
  • 三、Where(a => true) 邏輯表示式解析優化
  • 四、SaveManyToMany 聯級儲存多對多集合屬性
  • 五、遷移實體 - 到指定表名
  • 六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert
  • 七、ISelect.ToDelete 高階刪除
  • 八、全域性過濾器

以下的程式碼,先決定義程式碼如下 :

IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.Sqlite, @"Data Source=|DataDirectory|\db1.db;Max Pool Size=10";)
    .UseAutoSyncStructure(true) //自動同步實體結構到資料庫
    .Build();

public class Blog
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

一、Dto 對映查詢

class Dto
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int xxx { get; set; }
}

fsql.Select<Blog>().ToList<Dto>();
//SELECT Id, Url FROM Blog

fsql.Select<Blog>().ToList(a => new Dto { xxx = a.Rating} );
//SELECT Id, Url, Rating as xxx FROM Blog
//這樣寫,附加所有對映,再額外對映 xxx

fsql.Select<Blog>().ToList(a => new Blog { Id = a.Id }) 
//這樣寫,只查詢 id

fsql.Select<Blog>().ToList(a => new { a.Id }) 
//這樣寫,只查詢 id,返回匿名物件

對映支援單表/多表,是在查詢資料之前對映(不是先查詢所有欄位再到記憶體對映)

查詢規則,查詢屬性名,會迴圈內部物件 _tables(join 查詢後會增長),以 主表優先查,直到查到相同的欄位。

如:

A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被對映。也可以指定 id = C.id 對映。

友情提醒:在 dto 可以直接對映一個導航屬性


二、IncludeMany 聯級載入

之前已經實現,有設定關係,和未設定關係 的導航集合屬性聯級載入。

有設定關係的(支援一對多、多對多):

fsql.Select<Tag>().IncludeMany(a => a.Goods).ToList();

未設定關係的,臨時指定關係(只支援一對多):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Where(b => b.TagId == a.Id));

只查詢每項子集合的前幾條資料,避免像EfCore載入所有資料導致IO效能低下(比如某商品下有2000條評論):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Take(10));

上面已有的 IncludeMany 功能還不夠自由靈活。

新功能1:在 Dto 上做對映 IncludeMany

老的 IncludeMany 限制只能在 ISelect 內使用,必須要先查上級資料,解決這個問題我們做了直接在 Dto 上做對映:

查詢 Goods 商品表,分類1、分類2、分類3 各10條資料

//定義臨時類,也可以是 Dto 類
class Dto {
    public int TypeId { get; set; }
    public List<Goods > GoodsList { get; set; }
}

var dto = new [] { 1,2,3 }.Select(a => new Dto { TypeId = a }).ToList();
dto.IncludeMany(d => d.GoodsList.Take(10).Where(gd => gd.TypeId == d.TypeId));

//執行後,dto 每個元素.Vods 將只有 10條記錄

現在 IncludeMany 不再是 ISelect 的專利,普通的 List<T> 也可以用它來貪婪載入資料,並準確填充到內部各元素中。

新功能2:查詢子集合表的指定欄位

老的 IncludeMany 限制只能查子表的所有欄位,子表過段多過的話比較浪費 IO 效能。

新功能可以設定子集合返回部分欄位,避免子集合欄位過多的問題。

fsql.Select<Tag>().IncludeMany(a => a.Goods.Select(b => new Goods { Id = b.Id, Title = b.Title }));
//只查詢 goods 表 id, title 欄位,再作填充

三、Where(a => true) 邏輯表示式解析優化

相信很多 ORM 解析表示式的時候處理不了這個問題,我們之前已經解決了 99%。

這個月發現還有一餘孽未清,發現問題後及時解決了,並增加單元測試程式碼以絕後患。

四、SaveManyToMany 聯級儲存多對多集合屬性

在此之前,FreeSql.DbContext 和 倉儲實現,已經實現了聯級儲存功能,如下:

聯級儲存功能可實現儲存物件的時候,將其【OneToMany】、【ManyToMany】導航屬性集合也一併儲存。

全域性關閉:

fsql.SetDbContextOptions(opt => opt.EnableAddOrUpdateNavigateList = false);

區域性關閉:

var repo = fsql.GetRepository<T>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = false;

新功能:

儲存實體的指定【多對多】導航屬性,SaveManyToMany 方法實現在 BaseRepository、DbContext。

解決問題:當實體類導航資料過於複雜的時候,選擇關閉聯級儲存的功能是明智之選,但是此時【多對多】資料儲存功能寫起來非常繁瑣麻煩(因為要與現有資料對比後儲存)。

var song = new Song { Id = 1 };
song.Tags = new List<Tag>();
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
repo.SaveManyToMany(song, "Tags");
//輕鬆儲存 song 與 tag 表的關聯

機制規則與聯級儲存的【多對多】一樣,如下:

我們對中間表的儲存是完整對比操作,對外部實體的操作只作新增(注意不會更新)

  • 屬性集合為空時,刪除他們的所有關聯資料(中間表)
  • 屬性集合不為空時,與資料庫存在的關聯資料(中間表)完全對比,計算出應該刪除和新增的記錄

五、遷移實體 - 到指定表名

fsql.CodeFirst.SyncStructure(typeof(Log), "Log_1"); //遷移到 Log_1 表
fsql.CodeFirst.SyncStructure(typeof(Log), "Log_2"); //遷移到 Log_2 表

在此功能上,我們對分表功能做了點升級,以下動作都會做遷移動作:

fsql.Select<Log>().AsTable((_, oldname) => $"{oldname}_1");
fsql.GetRepository<Log>(null, oldname => $"{oldname}_1");

六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert

FreeSql 提供了多種插入或更新方法,v0.11 之前主要使用 FreeSql.Repository/FreeSql.DbContext 庫提供的方法實現。

FreeSql.Repository 之 InsertOrUpdate

此方法與 FreeSql.DbContext AddOrUpdate 方法功能一樣。

var repo = fsql.GetRepository<T>();
repo.InsertOrUpdate(實體);

如果內部的狀態管理存在資料,則更新。

如果內部的狀態管理不存在資料,同查詢資料庫,是否存在。

存在則更新,不存在則插入

缺點:不支援批量操作

新功能:MySql 特有功能 On Duplicate Key Update

FreeSql.Provider.MySql 和 FreeSql.Provider.MySqlConnector 在 v0.11.11 版本已支援 MySql 特有的功能,On Duplicate Key Update。

這個功能也可以實現插入或更新資料,並且支援批量操作。

class TestOnDuplicateKeyUpdateInfo
{
    [Column(IsIdentity = true)]
    public int id { get; set; }
    public string title { get; set; }
    public DateTime time { get; set; }
}

var item = new TestOnDuplicateKeyUpdateInfo { id = 100, title = "title-100", time = DateTime.Parse("2000-01-01") };
fsql.Insert(item)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`, `time`) VALUES(100, 'title-100', '2000-01-01 00:00:00.000')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = VALUES(`time`)

OnDuplicateKeyUpdate() 之後可以呼叫的方法:

方法名 描述
IgnoreColumns 忽略更新的列,機制和 IUpdate.IgnoreColumns 一樣
UpdateColumns 指定更新的列,機制和 IUpdate.UpdateColumns 一樣
Set 手工指定更新的列,與 IUpdate.Set 功能一樣
SetRaw 作為 Set 方法的補充,可傳入 SQL 字串
ToSql 返回即將執行的 SQL 語句
ExecuteAffrows 執行,返回影響的行數

IInsert 與 OnDuplicateKeyUpdate 都有 IgnoreColumns、UpdateColumns 方法。

當插入實體/集合實體的時候,忽略了 time 列,程式碼如下:

fsql.Insert(item)
    .IgnoreColumns(a => a.time)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`) VALUES(200, 'title-200')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = '2000-01-01 00:00:00.000'

我們發現,UPDATE time 部分變成了常量,而不是 VALUES(`time`),機制如下:

當 insert 部分中存在的列,在 update 中將以 VALUES(`欄位`) 的形式設定;

當 insert 部分中不存在的列,在 update 中將為常量形式設定,當操作實體陣列的時候,此常量為 case when ... end 執行(與 IUpdate 一樣);

新功能2:PostgreSQL 特有功能 On Conflict Do Update

使用方法 MySql OnDuplicateKeyUpdate 大致相同。


七、ISelect.ToDelete 高階刪除

預設 IDelete 不支援導航物件,多表關聯等。ISelect.ToDelete 可將查詢轉為刪除物件,以便支援導航物件或其他查詢功能刪除資料,如下:

fsql.Select<T1>().Where(a => a.Options.xxx == 1).ToDelete().ExecuteAffrows();

注意:此方法不是將資料查詢到記憶體迴圈刪除,上面的程式碼產生如下 SQL 執行:

DELETE FROM `T1` WHERE id in (select a.id from T1 a left join Options b on b.t1id = a.id where b.xxx = 1)

複雜刪除使用該方案的好處:

  • 刪除前可預覽測試資料,防止錯誤刪除操作;
  • 支援更加複雜的刪除操作(IDelete 預設只支援簡單的操作),甚至在 ISelect 上使用 Limit(10) 將只刪除附合條件的前 10條記錄;

還有 ISelect.ToUpdate 高階更新資料功能,使用方法類似


八、全域性過濾器

FreeSql 基礎層實現了 Select/Update/Delete 可設定的全域性過濾器功能。

public static AsyncLocal<Guid> TenantId { get; set; } = new AsyncLocal<Guid>();

fsql.GlobalFilter
    .Apply<TestAddEnum>("test1", a => a.Id == TenantId.Value)
    .Apply<AuthorTest>("test2", a => a.Id == 111)
    .Apply<AuthorTest>("test3", a => a.Name == "11");

Apply 泛型引數可以設定為任何型別,當使用 Select/Update/Delete 方法時會進行過濾器匹配嘗試(try catch):

  • 匹配成功的,將附加 where 條件;
  • 匹配失敗的,標記下次不再匹配,避免效能損耗;

如何禁用?

fsql.Select<TestAddEnum>().ToList(); //所有生效
fsql.Select<TestAddEnum>().DisableGlobalFilter("test1").ToList(); //禁用 test1
fsql.Select<TestAddEnum>().DisableGlobalFilter().ToList(); //禁用所有

fsql.Update/Delete 方法效果同上。

注意:IFreeSql.GlobalFilter 與 倉儲過濾器 不是一個功能,可以同時生效

鳴謝

感謝反饋 bug 的朋友!

倉庫地址:https://github.com/2881099/FreeSql

請移步更新日誌:https://github.com/2881099/FreeSql/wiki/%e6%9b%b4%e6%96%b0%e6%97%a5%e5%bf%97