02020408 EF Core基础08-单向导航属性、双向导航属性、关系配置一对多而不是多对一的好处、自引用的组织树结构
02020408 EF Core基础08-单向导航属性、双向导航属性、关系配置一对多而不是多对一的好处、自引用的组织树结构
1. 双向导航属性(视频3-17)
- 在02020407中,如上图示例的两个实体类就是双向导航属性
- 可以通过Article拿到Comment
- 可以通过Comment拿到Article
2. 单项导航属性
- 此时User表不知道Leave表的存在。
- 不设置反向的属性,然后配置的时候WithMany()不设置参数即可。
3. 单项导航属性示例
3.1 创建项目
- 创建SingleNavigation控制台项目
// SingleNavigation.csproj直接添加依赖包
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="microsoft.entityframeworkcore.sqlserver" Version="5.0.4" />
<PackageReference Include="microsoft.entityframeworkcore.tools" Version="5.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// User.cs
namespace SingleNavigation
{
class User // 用户
{
public long Id { get; set; }
public string Name { get; set; }
}
}
说明:没有到Leave的导航属性
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// UserConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace SingleNavigation
{
class UserConfig : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("T_Users");
}
}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Leave.cs
namespace SingleNavigation
{
class Leave // 申请单
{
public long Id { get; set; }
public User Requestor { get; set; } // 申请人
public User Approver { get; set; } // 批准人
public string Remarks { get; set; } // 备注
}
}
说明:有两个到User的的导航属性
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// LeaveConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace SingleNavigation
{
class LeaveConfig : IEntityTypeConfiguration<Leave>
{
public void Configure(EntityTypeBuilder<Leave> builder)
{
builder.ToTable("T_Leaves");
builder.HasOne<User>(l => l.Requestor).WithMany().IsRequired(); // 此时WithMany()方法不写参数
builder.HasOne<User>(l => l.Approver).WithMany(); // 此时WithMany()方法不写参数
}
}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
using System;
namespace SingleNavigation
{
class MyDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Leave> Leaves { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True;MultipleActiveResultSets=true";
optionsBuilder.UseSqlServer(connStr);
optionsBuilder.LogTo(Console.WriteLine);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
}
3.2 迁移数据库
PM> add-migration init
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
PM> update-database
Build started...
Build succeeded.
Applying migration '20250921003202_adddog'.
Done.
PM>
- 通过SSMS查看表结构
T_Leaves表 |
T_Users表 |
- 通过SSMS查看外键结构
外键1 |
外键2 |
3.3 数据库插入数据
using System;
using System.Linq;
namespace SingleNavigation
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
User us01 = new User { Name = "杨总" };
User us02 = new User { Name = "小科" };
Leave le01 = new Leave { Remarks = "回家处理拆迁事宜", Requestor = us02 }; // 通过这条语句来插入。
ctx.Leaves.Add(le01);
ctx.SaveChanges();
}
Console.WriteLine("数据插入成功!!!");
}
}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
Id RequestorId ApproverId Remarks
1 1 NULL 回家处理拆迁事宜
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
Id Name
1 小科 // 杨总没用到,因此没有。
3.4 查询示例
using System;
using System.Linq;
namespace SingleNavigation
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
var le01 = ctx.Leaves.FirstOrDefault(); // 如果没有为空
if(le01 != null)
{
Console.WriteLine(le01.Remarks);
}
}
Console.WriteLine("数据查询成功!!!");
}
}
}
控制台输出:
回家处理拆迁事宜
dbug: 2025/9/22 22:31:28.475 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)
'MyDbContext' disposed.
数据查询成功!!!
// 查看SQL语句
SELECT TOP(1) [t].[Id], [t].[ApproverId], [t].[Remarks], [t].[RequestorId]
FROM [T_Leaves] AS [t]
3.5 单项属性和双向属性的选择
对于主从结构的“一对多”表关系,一般是声明双向导航属性。
而对于其他的“一对多”表关系:如果表属于被很多表引用的基础表,则用单项导航属性,否则可以自由决定是否用双向导航属性。
4. 关系配置在任何一方都可以(视频3-18)
- 一对多(多对一),采用双向导航属性,正反配置都可以
// 一对多配置
CommentConfig:
builder.HasOne<Article>(c=>c.Article).WithMany(a => a.Comments).IsRequired();
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 多对一配置
ArticleConfig:
builder.HasMany<Comment>(a => a.Comments).WithOne(c => c.Article).IsRequired();
- 一对多(多对一),采用单项导航属性,配置在多的一端。如本章3.1小结示例。
// 一对多配置
builder.HasOne<User>(l => l.Requestor).WithMany().IsRequired(); // 此时WithMany()方法不写参数
- 总结:考虑到有单项导航属性的可能,我们一般用HasOne().WithMany()
- 在项目开发过程中,随着开发的深入,可能会有单项导航属性的要求,此时如果选择多对一的配置,修改起来会相对比较麻烦。
- 采用一对多的形式,就不用担心增加单项导航属性的需求。
5. 自引用的组织结构树(视频3-19)
5.1 父子节点
- 如上图,除了董事会(根节点),都有若干个子节点。设计一部、设计二部...等,可能有子节点。大多数节点既有父节点,又有子节点。
- 一个组织单元对应多个子组织单元;一个组织单元,只有一个父组织单元。这样一种特殊的一对多的组织单元称为自引用组织结构树。
5.2 表和配置类设计
// 实体类
class OrgUnit // 组织节点
{
public long Id { get; set; }
public string Name { get; set; }
public OrgUnit Parent { get; set; } // 组织节点的父节点,还是指向自己
public List<OrgUnit> Children { get; set; } = new List<OrgUnit>(); // 组织节点的子节点是一个泛型List,泛型还是自己
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 配置类
builder.ToTable("T_OrgUnits");
builder.Property(o => o.Name).IsRequired().IsUnicode().HasMaxLength(100);
builder.HasOne<OrgUnit>(u => u.Parent).WithMany(p => p.Children);
5.3 创建项目
- 创建SelfReferenceTree控制台项目
// SelfReferenceTree.csproj直接添加依赖包
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="microsoft.entityframeworkcore.sqlserver" Version="5.0.4" />
<PackageReference Include="microsoft.entityframeworkcore.tools" Version="5.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System.Collections.Generic;
namespace SelfReferenceTree
{
class OrgUnit // 组织节点
{
public long Id { get; set; }
public string Name { get; set; }
public OrgUnit Parent { get; set; } // 组织节点的父节点,还是指向自己
public List<OrgUnit> Children { get; set; } = new List<OrgUnit>(); // 组织节点的子节点是一个泛型List
}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace SelfReferenceTree
{
class OrgUnitConfig : IEntityTypeConfiguration<OrgUnit>
{
public void Configure(EntityTypeBuilder<OrgUnit> builder)
{
builder.ToTable("T_OrgUnits");
builder.Property(o => o.Name).IsUnicode().IsRequired().HasMaxLength(50);
builder.HasOne<OrgUnit>(u => u.Parent).WithMany(p => p.Children); // 因为根节点(董事会)是没有父节点的,因此可空,为不能IsRequired。
}
}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using Microsoft.EntityFrameworkCore;
using System;
namespace SelfReferenceTree
{
class MyDbContext : DbContext
{
public DbSet<OrgUnit> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True;MultipleActiveResultSets=true";
optionsBuilder.UseSqlServer(connStr);
optionsBuilder.LogTo(Console.WriteLine);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
}
5.4 查看表
T_OrgUnit表 |
外键关系 |
- 注意,这里是自引用的外键关系。
5.5 插入数据
// 插入数据形式1:无法完成所有数据插入数据库
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
namespace SelfReferenceTree
{
class Program
{
static void Main(string[] args)
{
OrgUnit ouRoot = new OrgUnit { Name = "中科集团全球总部" }; // 根节点
OrgUnit ouAisa = new OrgUnit { Name = "中科集团亚太总部" };
OrgUnit ouAmerica = new OrgUnit { Name = "中科集团亚美洲总部" };
OrgUnit ouUSA = new OrgUnit { Name = "中科美国" };
OrgUnit ouCAN = new OrgUnit { Name = "中科加拿大" };
OrgUnit ouCHN = new OrgUnit { Name = "中科中国" };
OrgUnit ouSG = new OrgUnit { Name = "中科新加坡" };
// 既可以设置一个OrgUnit的Parent,也可以把一个节点加入父节点的Children.Add(...)
ouUSA.Parent = ouAmerica;
ouCAN.Parent = ouAmerica;
ouAisa.Children.Add(ouCHN);
ouAisa.Children.Add(ouSG);
ouRoot.Children.Add(ouAmerica);
ouRoot.Children.Add(ouAisa);
using (MyDbContext ctx = new MyDbContext())
{
ctx.OrgUnits.Add(ouRoot); // 顺杆爬,都会被添加。
ctx.SaveChanges(); // 更新数据库
}
Console.WriteLine("数据插入成功!!!");
}
}
}
控制台输出:
数据插入成功!!!
// 查看数据库信息
Id Name ParentId
1 中科集团全球总部 NULL
2 中科集团亚美洲总部 1
3 中科集团亚太总部 1
4 中科中国 3
5 中科新加坡 3
注意,此时ouUSA和ouCAN没有保存到数据库,这里要将两个方向都建立起来。既要有爹,也要有孩子。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 插入数据形式2:可以完成所有数据插入数据库:有子节点就指子节点,有父节点就指父节点。这样关系比较乱,双向都指,不方便。
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
namespace SelfReferenceTree
{
class Program
{
static void Main(string[] args)
{
OrgUnit ouRoot = new OrgUnit { Name = "中科集团全球总部" }; // 根节点
OrgUnit ouAisa = new OrgUnit { Name = "中科集团亚太总部" };
OrgUnit ouAmerica = new OrgUnit { Name = "中科集团亚美洲总部" };
OrgUnit ouUSA = new OrgUnit { Name = "中科美国" };
OrgUnit ouCAN = new OrgUnit { Name = "中科加拿大" };
OrgUnit ouCHN = new OrgUnit { Name = "中科中国" };
OrgUnit ouSG = new OrgUnit { Name = "中科新加坡" };
// 既可以设置一个OrgUnit的Parent,也可以把一个节点加入父节点的Children.Add(...)
ouRoot.Children.Add(ouAmerica); // 指明子
ouRoot.Children.Add(ouAisa); // 指明子
ouAisa.Parent = ouRoot; // 指明父
ouAisa.Children.Add(ouCHN); // 指明子
ouAisa.Children.Add(ouSG); // 指明子
ouAmerica.Parent = ouRoot; // 指明父
ouAmerica.Children.Add(ouUSA); // 指明子
ouAmerica.Children.Add(ouCAN); // 指明子
using (MyDbContext ctx = new MyDbContext())
{
ctx.OrgUnits.Add(ouRoot); // 顺杆向上&向下爬,都会被添加。
ctx.SaveChanges(); // 更新数据库
}
Console.WriteLine("数据插入成功!!!");
}
}
}
控制台输出:
数据插入成功!!!
// 查看数据库
Id Name ParentId
1 中科集团全球总部 NULL
2 中科集团亚美洲总部 1
3 中科集团亚太总部 1
4 中科美国 2
5 中科加拿大 2
6 中科中国 3
7 中科新加坡 3
说明:现在所有数据都插入了。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 插入数据形式3:可以完成所有数据插入数据库:指出所有父节点,然后加入所有节点。推荐用法。
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
namespace SelfReferenceTree
{
class Program
{
static void Main(string[] args)
{
OrgUnit ouRoot = new OrgUnit { Name = "中科集团全球总部" }; // 根节点
OrgUnit ouAisa = new OrgUnit { Name = "中科集团亚太总部" };
OrgUnit ouAmerica = new OrgUnit { Name = "中科集团亚美洲总部" };
OrgUnit ouUSA = new OrgUnit { Name = "中科美国" };
OrgUnit ouCAN = new OrgUnit { Name = "中科加拿大" };
OrgUnit ouCHN = new OrgUnit { Name = "中科中国" };
OrgUnit ouSG = new OrgUnit { Name = "中科新加坡" };
// 指出所有父节点
ouCHN.Parent = ouAisa;
ouSG.Parent = ouAisa;
ouUSA.Parent = ouAmerica;
ouCAN.Parent = ouAmerica;
ouAisa.Parent = ouRoot;
ouAmerica.Parent = ouRoot;
using (MyDbContext ctx = new MyDbContext())
{
// 添加所有节点
ctx.Add(ouCHN);
ctx.Add(ouSG);
ctx.Add(ouUSA);
ctx.Add(ouCAN);
ctx.Add(ouAisa);
ctx.Add(ouAmerica);
ctx.SaveChanges(); // 更新数据库
}
Console.WriteLine("数据插入成功!!!");
}
}
}
控制台输出:
数据插入成功!!!
8 中科集团全球总部 NULL
9 中科集团亚太总部 8
10 中科集团亚美洲总部 8
11 中科中国 9
12 中科新加坡 9
13 中科美国 10
14 中科加拿大 10
5.6 查询数据
递归缩进打印
PrintChildren(int indentLevel, TestDbContext ctx,OrgUnit parent)
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// MyDbContext
using Microsoft.EntityFrameworkCore;
using System;
namespace SelfReferenceTree
{
class MyDbContext : DbContext
{
public DbSet<OrgUnit> OrgUnits { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connStr = "Server=.;Database=CoreDataDB;Trusted_Connection=True;MultipleActiveResultSets=true";
optionsBuilder.UseSqlServer(connStr);
// optionsBuilder.LogTo(Console.WriteLine); // 便于查看打印效果,将SQL语句先屏蔽
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Program.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
namespace SelfReferenceTree
{
class Program
{
static void Main(string[] args)
{
using(MyDbContext ctx = new MyDbContext())
{
OrgUnit myRoot = ctx.OrgUnits.Single(o => o.Parent == null); // 寻找最顶层的根节点。
Console.WriteLine(myRoot.Name); // 打印根节点
PrintChildren(1, ctx, myRoot); // 打印Parent的所有子节点,注意这里初始就用1,如果为0那么和根节点就并列了。
}
Console.WriteLine("数据查询成功!!!");
}
/// <summary>
/// 缩进打印Parent的所有子节点
/// </summary>
/// <param name="inentLeve1">缩进级别</param>
/// <param name="ctx"></param>
/// <param name="parent"></param>
static void PrintChildren(int identLeve1, MyDbContext ctx, OrgUnit parent)
{
var children = ctx.OrgUnits.Where(o => o.Parent == parent); // 找以Parent为根节点的所有子节点Children,即找我的子节点。
foreach (var child in children)
{
Console.WriteLine(new String('\t', identLeve1) + child.Name); // 注意\t用单引号,表示字符。打印Parent下的所有子节点。
PrintChildren(identLeve1 + 1, ctx, child); // 递归打印。打印所有子节点下的所有子节点。
}
}
}
}
控制台输出:
中科集团全球总部
中科集团亚太总部
中科中国
中科新加坡
中科集团亚美洲总部
中科美国
中科加拿大
数据查询成功!!!
5.7 项目总结
- 上示例是一个自己指向自己的特殊递归结构。从逻辑上来讲,自己指向自己和自己指向别人配置方法是一样的。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

浙公网安备 33010602011771号