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查看表结构
Typora-Logo
T_Leaves表

T_Users表
  • 通过SSMS查看外键结构
Typora-Logo
外键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 查看表
Typora-Logo
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※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-10-06 11:11  qinway  阅读(10)  评论(0)    收藏  举报