【问题标题】:EF Code First foreign key without navigation property没有导航属性的EF Code First外键
【发布时间】:2014-01-20 02:00:58
【问题描述】:

假设我有以下实体:

public class Parent
{
    public int Id { get; set; }
}
public class Child
{
    public int Id { get; set; }
    public int ParentId { get; set; }
}

什么是代码优先流式 API 语法来强制在数据库中创建 ParentId,并具有对 Parent 表的外键约束,不需要导航属性

我知道如果我将导航属性 Parent 添加到 Child,那么我可以这样做:

modelBuilder.Entity<Child>()
    .HasRequired<Parent>(c => c.Parent)
    .WithMany()
    .HasForeignKey(c => c.ParentId);

但在这种特殊情况下,我不想要导航属性。

【问题讨论】:

  • 我认为仅使用 EF 是不可能的,您可能需要在手动迁移中使用一些原始 SQL 来设置它
  • @LukeMcGregor 这就是我所担心的。如果你做出这个答案,我很乐意接受它,假设它是正确的。 :-)
  • 没有导航属性有什么具体原因吗?将导航属性设为私有为您工作 - 在实体外部不可见,但会取悦 EF。 (注意我还没有尝试过,但我认为这应该可行 - 看看这篇关于映射私有属性的帖子romiller.com/2012/10/01/…
  • 好吧,我不想要它,因为我不需要它。我不喜欢为了满足框架的要求而在设计中添加额外的不必要的东西。放入导航道具会杀了我吗?不。事实上,这就是我目前所做的。
  • 您总是需要至少一侧的导航属性来建立关系。欲了解更多信息stackoverflow.com/a/7105288/105445

标签: entity-framework ef-code-first


【解决方案1】:

使用 EF Code First Fluent API 是不可能的。您始终需要至少一个导航属性才能在数据库中创建外键约束。

如果您使用 Code First 迁移,您可以选择在包管理器控制台 (add-migration SomeNewSchemaName) 上添加新的基于代码的迁移。如果您对模型或映射进行了更改,则会添加新的迁移。如果您没有更改任何内容,请使用 add-migration -IgnoreChanges SomeNewSchemaName 强制进行新迁移。在这种情况下,迁移将只包含空的 UpDown 方法。

然后你可以修改Up方法,添加以下内容:

public override void Up()
{
    // other stuff...

    AddForeignKey("ChildTableName", "ParentId", "ParentTableName", "Id",
        cascadeDelete: true); // or false
    CreateIndex("ChildTableName", "ParentId"); // if you want an index
}

运行此迁移(包管理控制台上的update-database)将运行类似于此的 SQL 语句(对于 SQL Server):

ALTER TABLE [ChildTableName] ADD CONSTRAINT [FK_SomeName]
FOREIGN KEY ([ParentId]) REFERENCES [ParentTableName] ([Id])

CREATE INDEX [IX_SomeName] ON [ChildTableName] ([ParentId])

或者,无需迁移,您可以使用运行纯 SQL 命令

context.Database.ExecuteSqlCommand(sql);

其中context 是派生上下文类的实例,sql 只是上述 SQL 命令字符串形式。

请注意,尽管如此,EF 并不知道ParentId 是描述关系的外键。 EF 只会将其视为普通的标量属性。不知何故,与仅仅打开SQL管理工具并手动添加约束相比,以上所有方法只是一种更复杂,更慢的方式。

【讨论】:

  • 简化来自自动化:我无法访问部署我的代码的其他环境。能够在代码中执行这些更改对我来说很好。但我喜欢蛇 :)
  • 我想你也只是在实体上设置了一个属性,所以在父ID上,只需添加[ForeignKey("ParentTableName")]。这会将属性链接到父表上的任何键。现在你有了一个硬编码的表名。
  • 这显然不是不可能的,看下面的其他评论为什么这甚至被标记为正确答案
  • > 为什么这甚至被标记为正确答案——我猜是因为这个答案来自 2014 年,而 Jonatan Dragon 的实际正确答案来自 2019 年;)
【解决方案2】:

对于那些想要使用 DataAnotations 并且不想公开导航属性的人的小提示 - 使用 protected

public class Parent
{
    public int Id { get; set; }
}
public class Child
{
    public int Id { get; set; }
    public int ParentId { get; set; }

    protected virtual Parent Parent { get; set; }
}

就是这样 - 将创建在 Add-Migration 之后带有 cascade:true 的外键。

【讨论】:

  • 私人也可以
  • 创建 Child 时,您必须指定 Parent 还是只指定 ParentId
  • @MarcWittke virtual 属性不能是 private
  • @GeorgeMauer:这是个好问题!从技术上讲,任何一个都可以,但是当你有这样不一致的代码时,它也会成为问题,因为开发人员(尤其是新人)不确定要传入什么。
【解决方案3】:

虽然这篇文章是针对Entity Framework 而不是Entity Framework Core,但对于想要使用Entity Framework Core(我使用的是V1.1.2)实现相同目标的人来说可能会有用。

我不需要导航属性(尽管它们很好),因为我正在练习 DDD,我希望 ParentChild 成为两个单独的聚合根。我希望他们能够通过外键而不是通过特定于基础架构的 Entity Framework 导航属性相互交谈。

您所要做的就是使用HasOneWithMany 在一侧配置关系,而不指定导航属性(毕竟它们不存在)。

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

    protected override void OnModelCreating(ModelBuilder builder)
    {
        ......

        builder.Entity<Parent>(b => {
            b.HasKey(p => p.Id);
            b.ToTable("Parent");
        });

        builder.Entity<Child>(b => {
            b.HasKey(c => c.Id);
            b.Property(c => c.ParentId).IsRequired();

            // Without referencing navigation properties (they're not there anyway)
            b.HasOne<Parent>()    // <---
                .WithMany()       // <---
                .HasForeignKey(c => c.ParentId);

            // Just for comparison, with navigation properties defined,
            // (let's say you call it Parent in the Child class and Children
            // collection in Parent class), you might have to configure them 
            // like:
            // b.HasOne(c => c.Parent)
            //     .WithMany(p => p.Children)
            //     .HasForeignKey(c => c.ParentId);

            b.ToTable("Child");
        });

        ......
    }
}

我也给出了如何配置实体属性的示例,但这里最重要的是HasOne&lt;&gt;WithMany()HasForeignKey()

希望对你有帮助。

【讨论】:

  • 这是 EF Core 的正确答案,对于那些练习 DDD 的人来说,这是必须的。
  • 我不清楚删除导航属性发生了什么变化。你能澄清一下吗?
  • @andrew.rockwell:请参阅 HasOne&lt;Parent&gt;().WithMany() 的子配置。它们根本不引用导航属性,因为无论如何都没有定义导航属性。我会尽量通过我的更新使其更清晰。
  • 太棒了。谢谢@DavidLiang
  • @mirind4 与 OP 中的特定代码示例无关,如果您将不同的聚合根实体映射到它们各自的数据库表,根据 DDD,这些根实体只能通过其身份相互引用,并且不应包含对其他 AR 实体的完整引用。实际上,在 DDD 中,也很常见将实体的身份作为值对象,而不是使用诸如 int/log/guid(尤其是 AR 实体)之类的原始类型的 props,避免原始痴迷,并允许不同的 AR 通过值来引用实体对象 ID 类型。 HTH
【解决方案4】:

如果是 EF Core,您不一定需要提供导航属性。您可以简单地在关系的一侧提供外键。一个使用 Fluent API 的简单示例:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

namespace EFModeling.Configuring.FluentAPI.Samples.Relationships.NoNavigation
{
    class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
             modelBuilder.Entity<Post>()
                .HasOne<Blog>()
                .WithMany()
                .HasForeignKey(p => p.BlogId);
        }
    }

    public class Blog
    {
         public int BlogId { get; set; }
         public string Url { get; set; }
    }

    public class Post
    {
         public int PostId { get; set; }
         public string Title { get; set; }
         public string Content { get; set; }

        public int BlogId { get; set; }
    }
}

【讨论】:

  • 这个解决方案非常好,因为它使实体的定义更加精简、简单并且与正在使用的 ORM 松散耦合。
【解决方案5】:

使用导航属性的原因是类依赖。我将我的模型分成几个程序集,这些程序集可以在不同的项目中以任何组合使用或不使用。因此,如果我的实体具有从另一个程序集分类的导航属性,我需要引用该程序集,这是我想要避免的(或者任何使用该完整数据模型的一部分的项目都将携带所有内容)。

我有单独的迁移应用程序,用于迁移(我使用自动迁移)和初始数据库创建。该项目出于显而易见的原因引用了所有内容。

解决方案是 C 风格的:

  • 通过链接将带有目标类的文件“复制”到迁移项目(在VS中使用alt键拖放)
  • 通过#if _MIGRATION禁用导航属性(和FK属性)
  • 在迁移应用程序中设置该预处理器定义,而不是在模型项目中设置,因此它不会引用任何内容(在示例中不要引用带有Contact 类的程序集)。

示例:

    public int? ContactId { get; set; }

#if _MIGRATION
    [ForeignKey(nameof(ContactId))]
    public Contact Contact { get; set; }
#endif

当然,您应该以同样的方式禁用using 指令并更改命名空间。

之后,所有消费者都可以像往常一样使用该属性 DB 字段(如果不需要,不要引用其他程序集),但 DB 服务器会知道它是 FK 并且可以使用级联。非常肮脏的解决方案。但有效。

【讨论】:

    【解决方案6】:

    我正在使用 .Net Core 3.1、EntityFramework 3.1.3。 我一直在寻找,我想出的解决方案是使用HasForeginKey&lt;DependantEntityType&gt;(e =&gt; e.ForeginKeyProperty) 的通用版本。 您可以像这样创建一对一的关系:

    builder.entity<Parent>()
    .HasOne<Child>()
    .WithOne<>()
    .HasForeginKey<Child>(c => c.ParentId);
    
    builder.entity<Child>()
        .Property(c => c.ParentId).IsRequired();
    

    希望这对如何使用HasForeginKey 方法有所帮助或至少提供一些其他想法。

    【讨论】:

      猜你喜欢
      • 2017-06-26
      • 2016-01-09
      • 1970-01-01
      • 2022-07-18
      • 1970-01-01
      • 2013-10-21
      • 2016-10-10
      • 2016-03-16
      • 1970-01-01
      相关资源
      最近更新 更多