写在前面

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

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

  1. 我的BO之导航属性

    我的BO 1-我的BO之强类型 2-我的BO之数据保护 3-我的BO之状态控制 4-我的BO之导航属性 数据需要导航 数据之间普遍存在关系,做业务处理时往往也是按照关系在数据之间查询和处理.业务处理可 ...

  2. FreeSql (十八)导航属性

    导航属性是 FreeSql 的特色功能之一,可通过约定配置.或自定义配置对象间的关系. 导航属性有 OneToMany, ManyToOne, ManyToMany, OneToOne, Parent ...

  3. .NET ORM 导航属性【到底】可以解决什么问题?

    写在开头 从最早期入门时的单表操作, 到后来接触了 left join.right join.inner join 查询, 因为经费有限,需要不断在多表查询中折腾解决实际需求,不知道是否有过这样的经历 ...

  4. FreeSql (十七)联表查询

    FreeSql在查询数据下足了功能,链式查询语法.多表查询.表达式函数支持得非常到位. IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseConnect ...

  5. ASP.NET MVC深入浅出(被替换) 第一节: 结合EF的本地缓存属性来介绍【EF增删改操作】的几种形式 第三节: EF调用普通SQL语句的两类封装(ExecuteSqlCommand和SqlQuery ) 第四节: EF调用存储过程的通用写法和DBFirst模式子类调用的特有写法 第六节: EF高级属性(二) 之延迟加载、立即加载、显示加载(含导航属性) 第十节: EF的三种追踪

    ASP.NET MVC深入浅出(被替换)   一. 谈情怀-ASP.NET体系 从事.Net开发以来,最先接触的Web开发框架是Asp.Net WebForm,该框架高度封装,为了隐藏Http的无状态 ...

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

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-2  预先加载关联实体 问题 你想在一次数据交互中加载一个实体和与它相关联实体. ...

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

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-11  测试实体引用或实体集合是否加载 问题 你想测试关联实体或实体集合是否已经 ...

  8. 关系与导航属性(摘自微软MSDN)

    关系与导航属性 本主题概述实体框架如何管理实体间的关系.还对如何映射和操作关系提供了一些指南. 关系.导航属性和外键 在关系数据库中,表之间的关系(也称为关联)是通过外键定义的.外键 (FK) 是用于 ...

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

    一.首先了解下Entity Framework 自动关联查询: Entity Framework 自动关联查询,有三种方法:Lazy Loading(延迟加载),Eager Loading(预先加载) ...

随机推荐

  1. CodeForces 989D

    题意略. 思路: 可以看成是所有的云彩照常运动,而月亮在跑.只要两个云彩相交后,在分离前月亮能赶到,就算是符合题意的. 可以知道,两个相隔越远的相向运动地云彩是越有可能符合题意的,因为它们相遇所用时间 ...

  2. 82天突破1000star,项目团队梳理出软件开源必须注意的8个方面

    近期,我们在GitHub上开源了微服务任务调度框架SIA-TASK,82天,收获了1000+个star!由于这是SIA团队第一次开源项目,开源的相关工作,团队之前并没有太多的经验,因此我们特别整理了本 ...

  3. java学习之String类

    标签(空格分隔): String类 String 的概述 class StringDemo{ public static void main(String[] args){ String s1=&qu ...

  4. 学会spss就能找到数据分析工作吗

     大学课堂上学习了spss,老师也讲了很多知识,但是现在准备毕业了,我做的实习工作就是用业内的数据进行最新的行业研究.现在真正需要用到spss进行分析了,我却看不懂老板给的数据和分析要求,难道这就是理 ...

  5. SCRUM术语

    http://www.scrumcn.com/agile/scrum-knowledge-library/scrum.html#tab-id-2 Scrum: Scrum无对应中文翻译 Agile: ...

  6. IT项目经理入门心法

  7. 【管理学】SMART

  8. cogs2823求组合数(lucas定理

    http://cogs.pro:8080/cogs/problem/problem.php?pid=vNQJJVUVj 再写个数学水题,其实lucas适用于m,n比较大而p比较小的情况. 题意:给出两 ...

  9. codeforces 233 D. Table(思维+dp )

    题目链接:http://codeforces.com/contest/233/problem/D 题意:问在n*m的矩阵中满足在每一个n*n的矩阵里画k个点,一共有几种画法. 题解:其实这题挺简单的但 ...

  10. 徐州邀请赛 江苏 icpc I. T-shirt 矩阵快速幂

    题目 题目描述 JSZKC is going to spend his vacation! His vacation has N days. Each day, he can choose a T-s ...