曝光负能量传递正能量 發表於 2025-11-2 18:41:00

【EF Core】“多对多”关系与跳跃导航

<p>“多对多”关系不像“一对多”那么“单纯”,它内部涉及到“连接实体”(Join Entity)的概念。咱们先放下这个概念不表,来了解一下多对多数据表为什么需要一个“辅助表”来建立关系。</p>
<p>假设有两张表:一张表示学生,一张表示选修课。那么,这里头的关系是<span style="color: rgba(0, 0, 255, 1)"><strong>你可以选多门课,而一门课可以被多人选</strong></span>。这是多对多关系,没问题吧。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202511/367389-20251102104639929-1742110384.png" alt="image" width="418" height="113" loading="lazy"></p>
<p>按照数据库存储的原则,学生表中每位学生的信息都不应重复,而课程表也是如此。这么一看,多对多的关系不能直接在这两个表中创建了。</p>
<p>那就只能引入第三个表,专门保存前两个表的信息了。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202511/367389-20251102110601927-1000383864.png" alt="image" width="365" height="233" loading="lazy"></p>
<p>经过这样处理后,多对多的关系被拆解成两个一对多关系:</p>
<p><span style="color: rgba(0, 0, 128, 1)">左边:学生(1)--- 中间表(N);</span></p>
<p><span style="color: rgba(0, 0, 128, 1)">右边:课程(1)--- 中间表(N)。</span></p>
<p>这个中间表负责”连接“两个数据表。转换为实体类开,这个中间表就是”连接实体“了。</p>
<p>------------------------------------------------------------------------------------------------------------------------</p>
<p>接下来先弄个开胃菜,一个很简单的例子</p>
<p>1、定义实体。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Student
{
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">int</span> Id { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span> Name { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span>; } = <span style="color: rgba(0, 0, 255, 1)">null</span>!<span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span> Code { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span>; } = <span style="color: rgba(0, 0, 255, 1)">null</span>!<span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span>? Email { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 注意这个属性</span>
    <span style="background-color: rgba(255, 255, 153, 1)"><strong><span style="color: rgba(0, 0, 255, 1)">public</span> IList&lt;Course&gt; SelectedCourses { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span>; } = <span style="color: rgba(0, 0, 255, 1)">new</span> List&lt;Course&gt;</strong></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(255, 255, 153, 1)"><strong>();</strong></span>
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Course
{
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> Guid Id { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span> Name { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span>; } = <span style="color: rgba(0, 0, 255, 1)">null</span>!<span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span>? Tags { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 注意这个属性</span>
    <span style="background-color: rgba(255, 255, 153, 1)"><strong><span style="color: rgba(0, 0, 255, 1)">public</span> IList&lt;Student&gt; Students { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span>; } = <span style="color: rgba(0, 0, 255, 1)">new</span> List&lt;Student&gt;</strong></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(255, 255, 153, 1)"><strong>();</strong></span>
}</span></pre>
</div>
<p>实体类没什么,就是一个学生类,一个课程类。不过,请留意一下被标记的属性,后面会考。</p>
<p>2、定义数据库上下文。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> TestContext : DbContext
{
    </span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">override</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> OnConfiguring(DbContextOptionsBuilder ob)
    {
      ob.UseSqlServer(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">server=(localdb)\\mssqllocaldb;database=MySchool</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
    }

    </span><span style="color: rgba(0, 0, 255, 1)">#region</span> 数据集合
    <span style="color: rgba(0, 0, 255, 1)">public</span> DbSet&lt;Student&gt; StudentSet { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> DbSet&lt;Course&gt; CourseSet { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
    </span><span style="color: rgba(0, 0, 255, 1)">#endregion</span><span style="color: rgba(0, 0, 0, 1)">
}</span></pre>
</div>
<p>上下文这样就可以了,这里可以不写配置数据库模型的代码,因为 EF Core 内置的约定类会帮我们自动完成。</p>
<p>a、通过 DbContext 或子类定义的 DbSet 类型的属性,自动向模型添加 Student、Course 实体;</p>
<p>b、通过上面标记的特殊属性(你看,考点来了),自动识别出这是多对多的关系。</p>
<p>&nbsp; &nbsp; &nbsp; Student 类的&nbsp;SelectedCourses 属性导航到 Course;</p>
<p>&nbsp; &nbsp; &nbsp; Course 类的&nbsp;Students 属性导航到 Student。</p>
<p>两个导航属性都是集合类型,因此两者的关系是多对多。此处,SelectedCourses 和 Students 属性有个专用名字,叫“跳跃导航”(Skip Navigation)。这里不应该翻译为“跳过导航”,因为那样翻译意思就不太好理解,所以应取“跳跃”。</p>
<p>解释一下为什么会跳跃。还记得前文的分析吗?两个表如果是多对多关系,那么它们需要一个“连接”表来存储对应关系。也就是说,正常情况下,Student 类的导航属性应该指向中间实体(映射到连接表),Course 实体的导航属性也应该指向中间实体,再通过中间实体把二者连接起来。可是我们再回头看看示例,Student 的导航属性直接指向了 Course,而 Course 实体的导航属性也直接指向了 Student 实体。即<span style="color: rgba(0, 0, 128, 1)"><strong>它们都跨过(跳过)中间实体,两者直接连接起来了</strong></span>。</p>
<p>老周画了一个不专业的简图。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202511/367389-20251102171046605-1930218357.png" alt="image" width="555" height="270" loading="lazy"></p>
<p>这里也产生了一个疑问:我们没创建中间实体啊,难道是 EF Core 帮我们创建了?还真是,不妨打印一下数据库模型。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Main(<span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">[] args)
{
    </span><span style="color: rgba(0, 0, 255, 1)">using</span> <span style="color: rgba(0, 0, 255, 1)">var</span> context = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> TestContext();
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 获取数据库模型</span>
    IModel model =<span style="color: rgba(0, 0, 0, 1)"> context.Model;
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 打印</span>
<span style="color: rgba(0, 0, 0, 1)">    Console.WriteLine(model.ToDebugString());
}</span></pre>
</div>
<p>然后,运行代码,看看输出什么。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">Model:
EntityType: Course
    Properties:
      Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Name (</span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">) Required
      Tags (</span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">)
    <span style="background-color: rgba(255, 255, 0, 1)">Skip navigations:</span>
      <em><span style="background-color: rgba(255, 255, 0, 1)">Students (IList</span></em></span><em><span style="background-color: rgba(255, 255, 0, 1)">&lt;Student&gt;</span></em><span style="color: rgba(0, 0, 0, 1)"><em><span style="background-color: rgba(255, 255, 0, 1)">)</span></em> CollectionStudent Inverse: SelectedCourses
    Keys:
      Id PK
EntityType: <span style="background-color: rgba(204, 255, 204, 1)"><em><strong>CourseStudent (Dictionary</strong></em></span></span><span style="background-color: rgba(204, 255, 204, 1)"><em><strong>&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>, <span style="color: rgba(0, 0, 255, 1)">object</span>&gt;) CLR Type: Dictionary&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>, <span style="color: rgba(0, 0, 255, 1)">object</span>&gt;</strong></em></span><span style="color: rgba(0, 0, 0, 1)">
    Properties:
      SelectedCoursesId (no field, Guid) Indexer Required PK FK AfterSave:Throw
      StudentsId (no field, </span><span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)">) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      SelectedCoursesId, StudentsId PK
    Foreign keys:
      CourseStudent (Dictionary</span>&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>, <span style="color: rgba(0, 0, 255, 1)">object</span>&gt;) {<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">SelectedCoursesId</span><span style="color: rgba(128, 0, 0, 1)">'</span>} -&gt; Course {<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">Id</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">} Required Cascade   
      CourseStudent (Dictionary</span>&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>, <span style="color: rgba(0, 0, 255, 1)">object</span>&gt;) {<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">StudentsId</span><span style="color: rgba(128, 0, 0, 1)">'</span>} -&gt; Student {<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">Id</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">} Required Cascade
    Indexes:
      StudentsId
EntityType: Student
    Properties:
      Id (</span><span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)">) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Code (</span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">) Required
      Email (</span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">)
      Name (</span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)">) Required
    <span style="background-color: rgba(255, 255, 0, 1)">Skip navigations</span>:
      <em><span style="background-color: rgba(255, 255, 0, 1)">SelectedCourses (IList</span></em></span><em><span style="background-color: rgba(255, 255, 0, 1)">&lt;Course&gt;</span></em><span style="color: rgba(0, 0, 0, 1)"><em><span style="background-color: rgba(255, 255, 0, 1)">)</span></em> CollectionCourse Inverse: Students
    Keys:
      Id PK</span></pre>
</div>
<p>有没有发现多了一个实体,叫&nbsp;CourseStudent。虽然我们在代码中没有定义这样的类,但 EF Core 的&nbsp;ManyToManyJoinEntityTypeConvention 约定类会自动给数据库模型添加一个实体,类型是共享的 Dictionary&lt;string, object&gt;。这可是个万能实体类型,当你不想给项目定义一堆实体类时,你甚至可以把所有实体全注册为字典类型。当然,这样做对于面向对象,对阅读你代码的人来说就不友好了。</p>
<div class="cnblogs_code">
<pre> <span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">virtual</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> CreateJoinEntityType(
   </span><span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)"> joinEntityTypeName,
   IConventionSkipNavigation skipNavigation)
{
   </span><span style="color: rgba(0, 0, 255, 1)">var</span> model =<span style="color: rgba(0, 0, 0, 1)"> skipNavigation.DeclaringEntityType.Model;

   </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> DefaultPropertyBagType 就是字典类型</span>
   <span style="background-color: rgba(255, 255, 0, 1)"><strong><span style="color: rgba(0, 0, 255, 1)">var</span> joinEntityTypeBuilder = model.Builder.SharedTypeEntity(joinEntityTypeName, Model.DefaultPropertyBagType)!</strong></span><span style="color: rgba(0, 0, 0, 1)">;

   </span><span style="color: rgba(0, 0, 255, 1)">var</span> inverseSkipNavigation = skipNavigation.Inverse!<span style="color: rgba(0, 0, 0, 1)">;
   CreateSkipNavigationForeignKey(skipNavigation, joinEntityTypeBuilder);
   CreateSkipNavigationForeignKey(inverseSkipNavigation, joinEntityTypeBuilder);
}
    </span></pre>
</div>
<p>可以看看&nbsp;DefaultPropertyBagType 字段在 Model 类中的定义(Model 类从用途上不对外公开,但类本身是 public 的)。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">readonly</span> Type DefaultPropertyBagType = <span style="color: rgba(0, 0, 255, 1)">typeof</span>(<strong><span style="background-color: rgba(255, 255, 0, 1)">Dictionary&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>, <span style="color: rgba(0, 0, 255, 1)">object</span>&gt;</span></strong>);</pre>
</div>
<p>那这个自动添加的中间实体怎么命名呢?继续看源代码。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">virtual</span> <span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)"> GenerateJoinTypeName(IConventionSkipNavigation skipNavigation)
{
    </span><span style="color: rgba(0, 0, 255, 1)">var</span> inverseSkipNavigation =<span style="color: rgba(0, 0, 0, 1)"> skipNavigation.Inverse;
    Check.DebugAssert(
      inverseSkipNavigation</span>?.Inverse ==<span style="color: rgba(0, 0, 0, 1)"> skipNavigation,
      </span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Inverse's inverse should be the original skip navigation</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);

    </span><span style="color: rgba(0, 0, 255, 1)">var</span> declaringEntityType =<span style="color: rgba(0, 0, 0, 1)"> skipNavigation.DeclaringEntityType;
    </span><span style="color: rgba(0, 0, 255, 1)">var</span> inverseEntityType =<span style="color: rgba(0, 0, 0, 1)"> inverseSkipNavigation.DeclaringEntityType;
    </span><span style="color: rgba(0, 0, 255, 1)">var</span> model =<span style="color: rgba(0, 0, 0, 1)"> declaringEntityType.Model;
    </span><span style="color: rgba(0, 0, 255, 1)">var</span> joinEntityTypeName = !<span style="color: rgba(0, 0, 0, 1)">declaringEntityType.HasSharedClrType
      </span>?<span style="color: rgba(0, 0, 0, 1)"> declaringEntityType.ClrType.ShortDisplayName()
      : declaringEntityType.ShortName();
    </span><span style="color: rgba(0, 0, 255, 1)">var</span> inverseName = !<span style="color: rgba(0, 0, 0, 1)">inverseEntityType.HasSharedClrType
      </span>?<span style="color: rgba(0, 0, 0, 1)"> inverseEntityType.ClrType.ShortDisplayName()
      : inverseEntityType.ShortName();
    joinEntityTypeName </span>= StringComparer.Ordinal.Compare(joinEntityTypeName, inverseName) &lt; <span style="color: rgba(128, 0, 128, 1)">0</span>
      ? joinEntityTypeName +<span style="color: rgba(0, 0, 0, 1)"> inverseName
      : inverseName </span>+<span style="color: rgba(0, 0, 0, 1)"> joinEntityTypeName;

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (model.FindEntityType(joinEntityTypeName) != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">)
    {
      </span><span style="color: rgba(0, 0, 255, 1)">var</span> otherIdentifiers = model.GetEntityTypes().ToDictionary(et =&gt; et.Name, _ =&gt; <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">);
      joinEntityTypeName </span>=<span style="color: rgba(0, 0, 0, 1)"> Uniquifier.Uniquify(
            joinEntityTypeName,
            otherIdentifiers,
            </span><span style="color: rgba(0, 0, 255, 1)">int</span><span style="color: rgba(0, 0, 0, 1)">.MaxValue);
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> joinEntityTypeName;
}</span></pre>
</div>
<p>乱七八糟一大段,总结起来就是:</p>
<p><span style="color: rgba(0, 0, 128, 1)">1、分别获取跳跃导航两端的类型,即多对多关系中的两实体(Student 和 Course);</span></p>
<p><span style="color: rgba(0, 0, 128, 1)">2、将两实体的名称按字符排序,排在前面的作为前半段名字,排序在后面的作为后半段名字。比如,Student 与 Course 排序,字母 C 在 S 前面,所以,中间实体的名字就是 CourseStudent;</span></p>
<p><span style="color: rgba(0, 0, 128, 1)">3、向中间实体添加两个属性,两个属性共同构成主键。同时,它们也是外键,一个指向 Student,一个指向 Course。即这两个属性同时是主键和外键。</span></p>
<p>从中间实体到 Student 的导航叫“左边”,从中间实体到 Course 实体的导航叫 “右边”。</p>
<p>&nbsp;</p>
<p>如果咱们不想用 EF Core 约定的中间实体,也可以自己去定义。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> StudentCourseJoin
{
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> Student TheStudent { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span>; } = <span style="color: rgba(0, 0, 255, 1)">null</span>!<span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">public</span> Course TheCourse { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span>; } = <span style="color: rgba(0, 0, 255, 1)">null</span>!<span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<p>有大伙伴会说了,你这实体没有作为外键的属性啊。没事,外键属性可以作为影子属性(Shadow Property)来添加,反正有 TheStudent 等导航属性,不需要借助外键属性也可以引用其实体。</p>
<p>下面是非常复杂的配置代码,各位可以先让时间停止然后慢慢看。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> TestContext : DbContext
{
    ……

    </span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">override</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> OnModelCreating(ModelBuilder modelBuilder)
    {
      modelBuilder.Entity</span>&lt;Student&gt;<span style="color: rgba(0, 0, 0, 1)">()
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个学生选多门课</span>
                .HasMany(s =&gt;<span style="color: rgba(0, 0, 0, 1)"> s.SelectedCourses)
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一门课多位学生选</span>
                .WithMany(c =&gt;<span style="color: rgba(0, 0, 0, 1)"> c.Students)
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 中间实体</span>
                .UsingEntity&lt;StudentCourseJoin&gt;<span style="color: rgba(0, 0, 0, 1)">(
                  </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 右边:StudentCourseJoin &gt;&gt;&gt; Course
                  </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个 StudentCourseJoin 只引用一个 Course</span>
                  right =&gt; right.HasOne(e =&gt;<span style="color: rgba(0, 0, 0, 1)"> e.TheCourse)
                              </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个Course可引用多个StudentCourseJoin
                              </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 但此处省略了</span>
<span style="color: rgba(0, 0, 0, 1)">                              .WithMany()
                              </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 外键</span>
                              .HasForeignKey(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Course_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">),
                  </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 左边:StudentCourseJoin &gt;&gt;&gt; Student
                  </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个StudentCourseJoin引用一个Student</span>
                  left =&gt; left.HasOne(e =&gt;<span style="color: rgba(0, 0, 0, 1)"> e.TheStudent)
                              </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个Student可引用多个StudentCourseJoin
                              </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 但这里省略了</span>
<span style="color: rgba(0, 0, 0, 1)">                              .WithMany()
                              </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 外键</span>
                              .HasForeignKey(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Student_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">),

                  ent </span>=&gt;<span style="color: rgba(0, 0, 0, 1)">
                  {
                        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 因为这两个是影子属性,必须显式配置
                        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 否则找不到属性,会报错</span>
                        ent.Property&lt;<span style="color: rgba(0, 0, 255, 1)">int</span>&gt;(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Student_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
                        ent.Property</span>&lt;Guid&gt;(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Course_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
                        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 两个属性都是主键</span>
                        ent.HasKey(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Student_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Course_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
                  }
                );
    }
}</span></pre>
</div>
<p>最外层调用&nbsp;modelBuilder.Entity&lt;Student&gt;() 的代码就是配置 Student 和 Course 的关系的,相信各位都懂的。复杂的部分是 UsingEntity 方法开始的,配置中间实体(连接实体)的。</p>
<p>首先,咱们把中间实体的关系拆开:</p>
<p>A、Student 对中间实体:一对多,左边;</p>
<p>B、Course 对中间实体:一对多,右边。</p>
<p>所以,UsingEntity 方法的第一个委托配置右边。</p>
<div class="cnblogs_code">
<pre>right =&gt; right.HasOne(e =&gt;<span style="color: rgba(0, 0, 0, 1)"> e.TheCourse)
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个Course可引用多个StudentCourseJoin
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 但此处省略了</span>
<span style="color: rgba(0, 0, 0, 1)">            .WithMany()
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 外键</span>
            .HasForeignKey(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Course_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span>)</pre>
</div>
<p>不要问为什么,因为微软定义这个方法就是先右后左的。HasOne 就是从中间实体(StudentCourseJoin)出发,它引用了几个 Course?一个吧,嗯,所以是One嘛;然后 WithMany 反过来,Curse 可以引用几个中间实体?多个吧(不明白的可以想想,中间表里面是不是可以重复出现课程?)。因为 Course 类没有定义导航属性去引用中间实体,所以 WithMany 参数空白。最后是设置外键,谁引用谁?是中间实体引用 Course 吧,所以,需要一个叫 Course_ID 属性来保存课程ID。</p>
<p>好了,右边干完了,到左边了。</p>
<div class="cnblogs_code">
<pre>left =&gt; left.HasOne(e =&gt;<span style="color: rgba(0, 0, 0, 1)"> e.TheStudent)
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 一个Student可引用多个StudentCourseJoin
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 但这里省略了</span>
<span style="color: rgba(0, 0, 0, 1)">            .WithMany()
            </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 外键</span>
            .HasForeignKey(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Student_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span>)</pre>
</div>
<p>左边是谁跟谁?从中间实体出发,它可以引用几个 Student?一个吧,所以是 HasOne;反过来,Student 可以引用几个中间实体?由于学生可以多次出现在中间实体中,所以是 WithMany,但 Student 类没有指向中间实体的导航属性,所以参数空。最后是外键,谁引用谁?是中间实体引用 Student 类吧?所以,中间实体要有一个 Student_ID 属性来保存学生ID。</p>
<p>可是,Student_ID 和 Course_ID 在中间实体中是没有定义的属性,如果不手动配置,EF Core 是找不到的。</p>
<div class="cnblogs_code">
<pre>ent =&gt;<span style="color: rgba(0, 0, 0, 1)">
{
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 因为这两个是影子属性,必须显式配置
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 否则找不到属性,会报错</span>
    ent.Property&lt;<span style="color: rgba(0, 0, 255, 1)">int</span>&gt;(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Student_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
    ent.Property</span>&lt;Guid&gt;(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Course_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 两个属性都是主键</span>
    ent.HasKey(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Student_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Course_ID</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p>这两个属性因为实体类中没有定义,所以要作为影子属性用,然后是两个属性都是主键。完事了。</p>
<p>&nbsp;</p>
<p>这个代码你要是看懂了,说明你学习 EF Core 的境界又提高了。</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/tcjiaan/p/19184567
頁: [1]
查看完整版本: 【EF Core】“多对多”关系与跳跃导航