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

原文地址:https://www.cnblogs.com/kellynic/p/11610724.html

时间: 2024-08-10 05:55:57

FreeSql 导航属性的联级保存功能的相关文章

《Entity Framework 6 Recipes》中文翻译系列 (23) -----第五章 加载实体和导航属性之预先加载与Find()方法

翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-2  预先加载关联实体 问题 你想在一次数据交互中加载一个实体和与它相关联实体. 解决方案 假设你有如图5-2所示的模型. 图5-2 包含Customer和与它相关联信息的实体 和5-1节一样,在模型中,有一个Customer实体,一个与它关联的CustomerType和多个与它关联的CustomerEamil.它与CustomerType的关系是一对多关系,这是一个实体引用(译注:Cu

什么是块级元素和内联级元素(二)

block(块)元素的特点: ①总是在新行上开始: ②高度,行高以及外边距和内边距都可控制: ③宽度缺省是它的容器的100%,除非设定一个宽度. ④它可以容纳内联元素和其他块元素   inline元素的特点: ①和其他元素都在一行上: ②高,行高及外边距和内边距不可改变: ③宽度就是它的文字或图片的宽度,不可改变 ④内联元素只能容纳文本或者其他内联元素 (中文叫法有多种内联元素.内嵌元素.行内元素.直进式元素). 块元素(block element)和内联元素(inline element)都是

学习笔记-php简单联级下拉菜单输出-2016.4.7

在使用联级输出的时候一到一个模型里面的小问题,之前不注意,模型里不同的方法每次得到的数据交给控制器的时候,我都以数组的形式输出,但是这个数组我作为私有字段,到时今天做联级菜单输出的时候,不能正常输出,最后不得不吧这个私有字段去掉,在每个方法的作用域里面分别初始化一个数组,这样就互不干扰!(还是考虑的太少,太异想天开!) 不说了,贴代码 html部分 <select name="nav"> <option>请选择一个栏目类别</option> {$na

关于Entity Framework自动关联查询与自动关联更新导航属性对应的实体注意事项说明

一.首先了解下Entity Framework 自动关联查询: Entity Framework 自动关联查询,有三种方法:Lazy Loading(延迟加载),Eager Loading(预先加载),Explicit Loading(显式加载),其中Lazy Loading和Explicit Loading都是延迟加载. (注:由于Entity Framework版本的不同,以及采用不同的模式(DB First,Model First,Code First)来构建的Entity,最终导致可能自

显示属性、导航属性

声明:原创作品,转载时请注明文章来自SAP师太技术博客:www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将追究法律责任!原文链接:http://www.cnblogs.com/jiangzhengjun/p/4293923.html 导航属性查询效率低,因为是Join之后根据非关键字段进行查询,如果考虑性能方面的问题,则需要将此属性直接作为特征维度即可 特征与导航属性的区别: 我们是将客户名称做成导航属性还是直接做成特征的区别: 特征查询效率高(由

关系与导航属性

关系与导航属性 本主题概述实体框架如何管理实体间的关系.还对如何映射和操作关系提供了一些指南. 关系.导航属性和外键 在关系数据库中,表之间的关系(也称为关联)是通过外键定义的.外键 (FK) 是用于在两个表的数据之间建立并强制链接的一列或列组合.有三种关系类型:一对一.一对多和多对多.在一对多关系中,外键是在表示关系多端的表上定义的. 多对多关系涉及定义第三个表(也称为接合或联接表),主键由来自两个相关表的外键组成.在一对一关系中,主键还用作外键,两个表都没有单独的外键列. 下图显示的两个表存

《Entity Framework 6 Recipes》中文翻译系列 (28) ------ 第五章 加载实体和导航属性之测试实体是否加载与显式加载关联实体

翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-11  测试实体引用或实体集合是否加载 问题 你想测试关联实体或实体集合是否已经加载到上下文中,另外你想使用Code-First来管理数据访问. 解决方案 假设你有如图5-26所示的概念模型 图5-26 一个包含projects,managers和contractors的模型 在Visual Studio中添加一个名为Recipe11的控制台应用,并确保引用了实体框架6的库,NuGet可

富文本编辑器宽度自适应及取消自动保存功能

1.富文本编辑器宽度自适应设置 初始化富文本编辑器是 设置属性 initialFrameWidth: '100%' 2.富文本编辑器取消自动保存功能 在ueditor.all.js中找到 UE.registerUI('autosave', function(editor) { 这个方法并注释掉. 原文地址:https://www.cnblogs.com/wpp281154/p/11430976.html

用汇编语言给XP记事本添加“自动保存”功能 good

[文章标题]: 用汇编语言给XP记事本添加“自动保存”功能 [文章作者]: newjueqi [作者邮箱]:[email protected] [作者QQ]:190678908 [使用工具]: OD, LordPE,eXeScope[操作平台]: XP-SP2[作者声明]: 本人平时一般的文字处理都是用记事本(用Word好像大材小用了),电脑自从拿去大修后有时候会莫名其妙的重启,弄得经常重写(本人常常忘记保存^-^),于是想给记事本增加类似于Word的自动保存功能,以图个方便.失误之处敬请诸位大