FreeSql 導航屬性的聯級保存功能

寫在前面

FreeSql 一個款 .net 平臺下支持 .net framework 4.5+、.net core 2.1+ 的開源 ORM。單元測試超過3100+,正在不斷吸引新的開發者,生命不息開發不止。

和 EFCore 一樣,我們也有導航對象,支持【OneToOne】(一對一)、【ManyToOne】(多對一)、【OneToMany】(一對多)、【ParentChild】(父子)、【ManyToMany】(多對多),可以約定配置或手工配置實體間的關聯,也可以使用 fluent api 設置關聯。

聯級保存功能可實現保存對象的時候,將其【OneToMany】、【ManyToMany】導航屬性集合也一併保存,本文檔說明實現的機制防止誤用。

機制規則

【一對多】模型下, 保存時可聯級保存實體的屬性集合。出於使用安全考慮我們沒做完整對比,只實現實體屬性集合的添加或更新操作,所以不會刪除實體屬性集合的數據。

完整對比的功能使用起來太危險,試想下面的場景:

  • 保存的時候,實體的屬性集合是空的,如何操作?記錄全部刪除?
  • 保存的時候,由於數據庫中記錄非常之多,那麼只想保存子表的部分數據,或者只需要添加,如何操作?

【多對多】模型下,我們對中間表的保存是完整對比操作,對外部實體的操作只作新增(注意不會更新)

  • 屬性集合爲空時,刪除他們的所有關聯數據(中間表)
  • 屬性集合不爲空時,與數據庫存在的關聯數據(中間表)完整對比,計算出應該刪除和添加的記錄

功能開啓和關閉

IFreeSql fsql = new FreeSql.FreeSqlBuilder()

    .UseConnectionString(FreeSql.DataType.Sqlite, "Data Source=|DataDirectory|/document22.db;Pooling=true;Max Pool Size=10")

    .UseAutoSyncStructure(true) //自動同步結構到數據庫
    .UseMonitorCommand(cmd => Trace.WriteLine(cmd.CommandText)) //監聽SQL命令對象,在執行後
    .Build();

使用 FreeSqlBuilder 創建好的 IFreeSql 對象,聯級保存功能,默認是打開的。

全局關閉:

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

局部關閉:

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

一對多(OneToMany)代碼測試

爲了方便展示,以下是一個 ParentChild 關係,其實他也是 OneToMany,只不過是自己指向自己。

[Table(Name = "EAUNL_OTMP_CT")]
class CagetoryParent
{
    public Guid Id { get; set; }
    public string Name { get; set; }

    public Guid ParentId { get; set; }
    [Navigate("ParentId")]
    public List<CagetoryParent> Childs { get; set; }
}

初始化測試數據:

var cts = new[] {
    new CagetoryParent
    {
        Name = "分類1",
        Childs = new List<CagetoryParent>(new[]
        {
            new CagetoryParent { Name = "分類1_1" },
            new CagetoryParent { Name = "分類1_2" },
            new CagetoryParent { Name = "分類1_3" }
        })
    },
    new CagetoryParent
    {
        Name = "分類2",
        Childs = new List<CagetoryParent>(new[]
        {
            new CagetoryParent { Name = "分類2_1" },
            new CagetoryParent { Name = "分類2_2" }
        })
    }
};

1、執行批量插入:

var repo = g.sqlite.GetRepository<CagetoryParent>();
repo.Insert(cts);

初始執行該方法時,會執行自動創建數據庫表操作。如果表已存在,則執行對比,若無變化則不執行操作。

經過斷點調試,在控制檯可以看到輸出 SQL 內容爲:

INSERT INTO "EAUNL_OTMP_CT"("Id", "Name", "ParentId") VALUES('5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f', '分類1', '00000000-0000-0000-0000-000000000000'), ('5d90afcb-ed57-f6f4-0082-cb6c5b531b3e', '分類2', '00000000-0000-0000-0000-000000000000')

INSERT INTO "EAUNL_OTMP_CT"("Id", "Name", "ParentId") VALUES('5d90afcb-ed57-f6f4-0082-cb6d0c1c5f1a', '分類1_1', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afcb-ed57-f6f4-0082-cb6e74bd8eef', '分類1_2', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afcb-ed57-f6f4-0082-cb6f6267cc5f', '分類1_3', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afcb-ed57-f6f4-0082-cb7057c41d46', '分類2_1', '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e'), ('5d90afcb-ed57-f6f4-0082-cb7156e0375e', '分類2_2', '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e')

2、測試批量修改:

cts[0].Name = "分類11";
cts[0].Childs.Clear();
cts[1].Name = "分類22";
cts[1].Childs.Clear();
repo.Update(cts);

控制檯看到輸出 SQL 內容爲:

UPDATE "EAUNL_OTMP_CT" SET "Name" = CASE "Id" 
WHEN '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f' THEN '分類11' 
WHEN '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e' THEN '分類22' END 
WHERE ("Id" IN ('5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f','5d90afcb-ed57-f6f4-0082-cb6c5b531b3e'))

Childs.Clear 執行了,但是控制檯沒有輸出執行刪除子集合語句,說明沒有做完整的對比

3、子集合表已存在數據,繼續添加數據

cts[0].Name = "分類111";
cts[0].Childs.Clear();
cts[0].Childs.Add(new CagetoryParent { Name = "分類1_33" });
cts[1].Name = "分類222";
cts[1].Childs.Clear();
cts[1].Childs.Add(new CagetoryParent { Name = "分類2_22" });
repo.Update(cts);

控制檯看到輸出 SQL 內容爲:

UPDATE "EAUNL_OTMP_CT" SET "Name" = CASE "Id" 
WHEN '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f' THEN '分類111' 
WHEN '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e' THEN '分類222' END 
WHERE ("Id" IN ('5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f','5d90afcb-ed57-f6f4-0082-cb6c5b531b3e'))

INSERT INTO "EAUNL_OTMP_CT"("Id", "Name", "ParentId") VALUES('5d90afe8-ed57-f6f4-0082-cb725df546ea', '分類1_33', '5d90afcb-ed57-f6f4-0082-cb6b78eaaf9f'), ('5d90afe8-ed57-f6f4-0082-cb7338a6214c', '分類2_22', '5d90afcb-ed57-f6f4-0082-cb6c5b531b3e')

再一次驗證了【一對多】(OneToMany) 不會作完整對比,只會添加或更新,添加測試數據的時候用它能簡化好多代碼。

多對多(ManyToMany)代碼測試

以下我們創建了三個類,Song 爲本體類,Tag 爲外部類,SongTag 爲 中間關聯數據類,採用命名約定的方式進行了導航關係設置。

[Table(Name = "EAUNL_MTM_SONG")]
class Song
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public List<Tag> Tags { get; set; }
}
[Table(Name = "EAUNL_MTM_TAG")]
class Tag
{
    public Guid Id { get; set; }
    public string TagName { get; set; }
    public List<Song> Songs { get; set; }
}
[Table(Name = "EAUNL_MTM_SONGTAG")]
class SongTag
{
    public Guid SongId { get; set; }
    public Song Song { get; set; }
    public Guid TagId { get; set; }
    public Tag Tag { get; set; }
}

初始化測試數據:

var tags = new[] {
    new Tag { TagName = "流行" },
    new Tag { TagName = "80後" },
    new Tag { TagName = "00後" },
    new Tag { TagName = "搖滾" }
};
var ss = new[]
{
    new Song
    {
        Name = "愛你一萬年.mp3",
        Tags = new List<Tag>(new[]
        {
            tags[0], tags[1]
        })
    },
    new Song
    {
        Name = "李白.mp3",
        Tags = new List<Tag>(new[]
        {
            tags[0], tags[2]
        })
    }
};

1、執行批量插入:

var repo = g.sqlite.GetRepository<Song>();
repo.Insert(ss);

初始執行該方法時,會執行自動創建數據庫表操作。如果表已存在,則執行對比,若無變化則不執行操作。

經過斷點調試,在控制檯可以看到輸出 SQL 內容爲:

INSERT INTO "EAUNL_MTM_SONG"("Id", "Name") VALUES('5d90fdb3-6a6b-2c58-00c8-37974177440d', '愛你一萬年.mp3'), ('5d90fdb3-6a6b-2c58-00c8-37987f29b197', '李白.mp3')

INSERT INTO "EAUNL_MTM_TAG"("Id", "TagName") VALUES('5d90fdb7-6a6b-2c58-00c8-37991ead4f05', '流行'), ('5d90fdbd-6a6b-2c58-00c8-379a0432a09c', '80後')

INSERT INTO "EAUNL_MTM_SONGTAG"("SongId", "TagId") VALUES('5d90fdb3-6a6b-2c58-00c8-37974177440d', '5d90fdb7-6a6b-2c58-00c8-37991ead4f05'), ('5d90fdb3-6a6b-2c58-00c8-37974177440d', '5d90fdbd-6a6b-2c58-00c8-379a0432a09c')

INSERT INTO "EAUNL_MTM_TAG"("Id", "TagName") VALUES('5d90fdcc-6a6b-2c58-00c8-379b5af59d25', '00後')

INSERT INTO "EAUNL_MTM_SONGTAG"("SongId", "TagId") VALUES('5d90fdb3-6a6b-2c58-00c8-37987f29b197', '5d90fdb7-6a6b-2c58-00c8-37991ead4f05'), ('5d90fdb3-6a6b-2c58-00c8-37987f29b197', '5d90fdcc-6a6b-2c58-00c8-379b5af59d25')

2、測試批量更新,並且中間表數據有了變化

ss[0].Name = "愛你一萬年.mp5";
ss[0].Tags.Clear();
ss[0].Tags.Add(tags[0]);
ss[1].Name = "李白.mp5";
ss[1].Tags.Clear();
ss[1].Tags.Add(tags[3]);
repo.Update(ss);

控制檯看到輸出 SQL 內容爲:

UPDATE "EAUNL_MTM_SONG" SET "Name" = CASE "Id" 
WHEN '5d90fdb3-6a6b-2c58-00c8-37974177440d' THEN '愛你一萬年.mp5' 
WHEN '5d90fdb3-6a6b-2c58-00c8-37987f29b197' THEN '李白.mp5' END 
WHERE ("Id" IN ('5d90fdb3-6a6b-2c58-00c8-37974177440d','5d90fdb3-6a6b-2c58-00c8-37987f29b197'))

SELECT a."SongId", a."TagId" 
FROM "EAUNL_MTM_SONGTAG" a 
WHERE (a."SongId" = '5d90fdb3-6a6b-2c58-00c8-37974177440d')

DELETE FROM "EAUNL_MTM_SONGTAG" WHERE ("SongId" = '5d90fdb3-6a6b-2c58-00c8-37974177440d' AND "TagId" = '5d90fdbd-6a6b-2c58-00c8-379a0432a09c')

INSERT INTO "EAUNL_MTM_TAG"("Id", "TagName") VALUES('5d90febd-6a6b-2c58-00c8-379c21acfc72', '搖滾')

SELECT a."SongId", a."TagId" 
FROM "EAUNL_MTM_SONGTAG" a 
WHERE (a."SongId" = '5d90fdb3-6a6b-2c58-00c8-37987f29b197')

DELETE FROM "EAUNL_MTM_SONGTAG" WHERE ("SongId" = '5d90fdb3-6a6b-2c58-00c8-37987f29b197' AND "TagId" = '5d90fdb7-6a6b-2c58-00c8-37991ead4f05' OR "SongId" = '5d90fdb3-6a6b-2c58-00c8-37987f29b197' AND "TagId" = '5d90fdcc-6a6b-2c58-00c8-379b5af59d25')

INSERT INTO "EAUNL_MTM_SONGTAG"("SongId", "TagId") VALUES('5d90fdb3-6a6b-2c58-00c8-37987f29b197', '5d90febd-6a6b-2c58-00c8-379c21acfc72')

執行的過程如下:

  • 第一步,批量更新 song 數據
  • 第二步,由於是 song 是更新操作,所以需要先查出 song 的關聯數據
  • 第三步,刪除 song 的關聯數據(tags[0] 除外),因爲 tags[0] 是本次保存有的數據,直白的說就是刪除非本次保存的所有關聯數據
  • 第四步,添加 tags[3] 搖滾外部數據,因爲它還不存在外部表
  • 第五步,與第二步相同
  • 第六步,與第三步相同
  • 第七步,插入中間表數據,李白.mp5 與 搖滾 關聯

爲什麼會有這麼多步呢?原因是 song 測試數據是兩條,double 了,如果單條記錄大概是 4-5 條,取決於是否有新增的關聯數據需要添加。

3、測試清空關聯數據

ss[0].Name = "愛你一萬年.mp4";
ss[0].Tags.Clear();
ss[1].Name = "李白.mp4";
ss[1].Tags.Clear();
repo.Update(ss);

控制檯看到輸出 SQL 內容爲:

DELETE FROM "EAUNL_MTM_SONGTAG" WHERE ("SongId" = '5d90fdb3-6a6b-2c58-00c8-37974177440d')

DELETE FROM "EAUNL_MTM_SONGTAG" WHERE ("SongId" = '5d90fdb3-6a6b-2c58-00c8-37987f29b197')

UPDATE "EAUNL_MTM_SONG" SET "Name" = CASE "Id" 
WHEN '5d90fdb3-6a6b-2c58-00c8-37974177440d' THEN '愛你一萬年.mp4' 
WHEN '5d90fdb3-6a6b-2c58-00c8-37987f29b197' THEN '李白.mp4' END 
WHERE ("Id" IN ('5d90fdb3-6a6b-2c58-00c8-37974177440d','5d90fdb3-6a6b-2c58-00c8-37987f29b197'))

再一次證明【ManyToMany】(多對多) 模型下,中間表是完整的對比操作,外部表只會插入,不更新。

導航對象

除了聯級保存功能外,導航對象的主要設計目的爲快速在實體間點點點穿插,以便執行 lambda 表達式的查詢操作。

如何自定義導航關係?

//導航屬性,OneToMany
[Navigate("song_id")]
public virtual List<song_tag> Obj_song_tag { get; set; }

//導航屬性,ManyToOne/OneToOne
[Navigate("song_id")]
public virtual Song Obj_song { get; set; }

//導航屬性,ManyToMany
[Navigate(ManyToMany = typeof(tag_song))]
public virtual List<tag> tags { get; set; }
  • 可約定,可不約定;
  • 不約定的,需指定 Navigate 特性關聯;
  • 無關聯的,查詢時可以指明 On 條件,LeftJoin(a => a.Parent.Id == a.ParentId);
  • 已關聯的,直接使用導航對象就行,On 條件會自動附上;

也可以使用 FluentApi 在外部設置導航關係:

fsql.CodeFirst.ConfigEntity<實體類>(a => a
    .Navigate(b => b.roles, null, typeof(多對多中間實體類))
    .Navigate(b => b.users, "uid")
);

優先級,特性 > FluentApi

寫在最後

FreeSql 發佈已經10個月了,元旦將發佈 1.0 正式版,希望將來可以成爲 .net 社區下給力的輪子,也算是我不枉十幾年對 .net 不離不棄的一點貢獻吧。

希望 FreeSql 越來越好,

原 .net core 越來越好!(雖然 3.0 升級很多人翻了車,有心中那些情懷在,翻了車最多是罵幾句而已,罵完還得接着用它)

教程地址:《FreeSql 新手上路系列教程已發佈在 cnblogs》

源碼地址:https://github.com/2881099

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章