当前位置: 首页 > 工具软件 > FreeSQL > 使用案例 >

FreeSql 导航属性的联级保存功能

陈高寒
2023-12-01

  写在前面

  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();

  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 Childs { get; set; }

  }

  初始化测试数据:

  var cts=new[] {

  new CagetoryParent

  {

  Name="分类1",

  Childs=new List(new[]

  {

  new CagetoryParent { Name="分类1_1" },

  new CagetoryParent { Name="分类1_2" },

  new CagetoryParent { Name="分类1_3" }

  })

  },

  new CagetoryParent

  {

  Name="分类2",

  Childs=new List(new[]

  {

  new CagetoryParent { Name="分类2_1" },

  new CagetoryParent { Name="分类2_2" }

  })

  }

  };

  1、执行批量插入:

  var repo=g.sqlite.GetRepository();

  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 Tags { get; set; }

  }

  [Table(Name="EAUNL_MTM_TAG")]

  class Tag

  {

  public Guid Id { get; set; }

  public string TagName { get; set; }

  public List 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(new[]

  {

  tags[0], tags[1]

  })

  },

  new Song

  {

  Name="李白.mp3",

  Tags=new List(new[]

  {

  tags[0], tags[2]

  })

  }

  };

  1、执行批量插入:

  var repo=g.sqlite.GetRepository();

  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 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 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 越来越好,

 类似资料: