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

FreeSql是一款支持多种.NET框架版本的开源ORM,支持多种实体间的关系配置,如一对一、多对一、一对多、多对多等。本文介绍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();

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

<think>我们正在讨论FreeSql中使用复合对象属性的方法。复合属性通常指的是实体类中的属性本身也是一个自定义对象(即值对象),而不是简单的标量类型(如int、string等)。在ORM中,这种设计称为“复杂类型”或“嵌入类型”。 在FreeSql中,处理复合对象属性可以通过几种方式实现: 1. 使用`[Column(MapType = typeof(嵌入类型))]`特性,但这种方式在新版本中可能不推荐。 2. 使用`[Owned]`特性(类似于EF Core的Owned Entity)来标记该属性为嵌入类型。 3. 通过Fluent API配置。 根据引用[4]中提到的EF Core的配置方式(虽然用户问的是FreeSql,但我们可以参考ORM的通用思路),我们可以推测FreeSql可能有类似的配置方法。 然而,由于引用中并没有直接提到FreeSql的复合属性配置,我们需要结合FreeSql的文档(虽然未提供,但根据常见用法)来回答。 根据FreeSql的官方文档(这里根据知识补充),我们可以这样使用: ### 使用`[Owned]`特性 在值对象(复合属性)的类上标记`[Owned]`特性,然后在实体类中直接使用该类型作为属性。 ### 使用Fluent API配置 在DbContext(或类似配置)中,通过Fluent API配置实体时,使用`OwnsOne`方法。 ### 示例 假设我们有一个`Customer`实体,它有一个`Address`类型的复合属性(值对象)`HomeAddress`。 ```csharp public class Address { public string Street { get; set; } public string City { get; set; } public string ZipCode { get; set; } } public class Customer { public int Id { get; set; } public string Name { get; set; } public Address HomeAddress { get; set; } // 复合属性 } ``` 在FreeSql中,我们可以这样配置: #### 方法1:使用`[Owned]`特性(需要FreeSql 3.2.800及以上版本) 在`Address`类上加上`[Owned]`特性: ```csharp [Owned] public class Address { public string Street { get; set; } public string City { get; set; } public string ZipCode { get; set; } } ``` #### 方法2:使用Fluent API配置 在DbContext的`OnModelCreating`方法中(或使用`FluentApi`配置)进行配置: ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Customer>(builder => { builder.OwnsOne(c => c.HomeAddress, address => { address.Property(a => a.Street).HasMaxLength(100); address.Property(a => a.City).HasMaxLength(50); address.Property(a => a.ZipCode).HasMaxLength(10); }); }); } ``` 这样配置后,FreeSql会将`HomeAddress`对象的所有属性映射到同一张表中(即`Customer`表),字段名默认为`HomeAddress_Street`、`HomeAddress_City`等。你也可以通过Fluent API修改列名。 ### 注意事项 1. 复合属性(嵌入类型)不支持作为主键。 2. 嵌入类型不能有导航属性(即不能引用其他实体)。 3. 嵌入类型不能有自己的键(即不能有Id属性作为主键)。 ### 总结 在FreeSql中使用复合对象属性,可以通过`[Owned]`特性或Fluent API的`OwnsOne`方法进行配置。这样可以将值对象的属性映射到实体对应的数据库表中,而不是拆分成另一张表。 如果需要对嵌入类型进行更详细的配置(如列名、类型等),可以使用Fluent API。 §§相关问题§§ 1. 在FreeSql中,如何配置复合属性的列名? 2. FreeSql是否支持多个复合属性(即一个实体中有多个嵌入类型)? 3. FreeSql的嵌入类型和EF Core的嵌入类型有什么区别? </think>在 FreeSql 中使用复合对象属性(即对象嵌套对象的场景),需要通过 **嵌入对象(Embedded Objects)** 或 **值对象(Value Objects)** 的特性来实现。以下是具体实现方法: --- ### 一、定义嵌入对象 使用 `[Column(CanInsert = false, CanUpdate = false)]` 特性标记嵌套属性,并通过 `Name` 指定数据库列名映射: ```csharp public class Address // 复合对象 { public string Province { get; set; } public string City { get; set; } } public class User { public int Id { get; set; } [Column(CanInsert = false, CanUpdate = false)] public Address HomeAddress { get; set; } // 复合属性 // 显式映射到数据库字段 [Column(Name = "HomeAddress_Province")] public string Province { get => HomeAddress?.Province; } [Column(Name = "HomeAddress_City")] public string City { get => HomeAddress?.City; } } ``` --- ### 二、使用 FluentAPI 配置 在 `DbContext` 中通过 `ModelBuilder` 动态配置映射: ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>(eb => { eb.Property(u => u.HomeAddress.Province).HasColumnName("HomeProvince"); eb.Property(u => u.HomeAddress.City).HasColumnName("HomeCity"); }); } ``` --- ### 三、查询时加载复合属性 使用 `Include`/`IncludeMany` 加载嵌套对象: ```csharp var user = fsql.Select<User>() .Include(u => u.HomeAddress) // 显式加载复合属性 .Where(u => u.Id == 1) .First(); ``` --- ### 四、插入/更新复合属性 直接操作复合对象的属性: ```csharp var user = new User { HomeAddress = new Address { Province = "江苏", City = "苏州" } }; fsql.Insert(user).ExecuteAffrows(); // 自动映射到数据库字段 ``` --- ### 关键注意事项 1. **字段映射** 复合对象的属性需显式映射到数据库字段(通过特性或 FluentAPI),否则 FreeSql 不会自动处理嵌套结构。 2. **查询优化** 使用 `Include` 加载嵌套对象,避免 N+1 查询问题。 3. **不支持深度嵌套** 目前 FreeSql 不支持超过一层的对象嵌套(如 `User.HomeAddress.Location.Street`),需手动拆解映射。 4. **与 EF Core 的区别** FreeSql 没有原生 `[Owned]` 特性,需手动配置字段映射(而 EF Core 支持 `OwnsOne` 自动映射[^4])。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值