一对一关系

当一个实体最多与其他一个实体关联时,将使用一对一关系。 例如,一个Blog具有一个BlogHeader,并且BlogHeader属于一个Blog

本文档围绕许多示例进行结构化。 这些示例以常见情况开头,这些用例也引入了概念。 后面的示例涵盖了不太常见的配置类型。 此处的一个好方法是了解前几个示例和概念,然后根据具体需求转到后面的示例。 基于此方法,我们将从简单的“必需”和“可选”一对一关系开始。

小窍门

以下所有示例的代码都可以在 OneToOne.cs中找到。

必须一对一进行

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

一对一关系由以下组成:

  • 主体实体上的一个或多个 主键或备用键 属性。 例如,Blog.Id
  • 依赖实体上的一个或多个 外键 属性。 例如,BlogHeader.BlogId
  • (可选)引用导航 指向依赖实体的主体实体。 例如,Blog.Header
  • 可选项:在引用主体实体的依赖实体上设置引用导航。 例如,BlogHeader.Blog

小窍门

一对一关系哪一方应是主体,哪一方应是依赖方并不总是显而易见的。 一些注意事项包括:

  • 如果两种类型的数据库表已存在,则具有外键列的表必须映射到依赖类型。
  • 如果类型在没有其他类型的情况下无法以逻辑方式存在,则类型通常是依赖类型。 例如,对于不存在的博客,具有标头是没有意义的,因此 BlogHeader 自然是依赖类型。
  • 在自然的父母/子女关系中,子女通常是从属类型。

因此,对于本示例中的关系:

  • 外键属性 BlogHeader.BlogId 不可为 NULL。 这使得关系“必需”,因为每个依赖项(BlogHeader必须与某些主体(Blog)相关,因为其外键属性必须设置为某些值。
  • 这两个实体都有指向关系另一端相关实体的导航。

注释

必需的关系可确保每个依赖实体都必须与某些主体实体相关联。 但是, 主体实体始终 可以不存在,而无需任何依赖实体。 也就是说,必需的关系 并不 表示将始终存在依赖实体。 EF 模型中没有办法,也没有关系数据库中的标准方法,以确保主体与依赖数据库相关联。 如果需要,则必须在应用程序(业务)逻辑中实现它。 有关详细信息 ,请参阅“必需”导航

小窍门

具有两个导航的关系——一个是从依赖方到主方,另一个是从主方到依赖方的逆向——称为双向关系。

此关系 由约定发现。 即:

  • Blog 被发现为关系中的主体,BlogHeader 被发现为被依赖者。
  • BlogHeader.BlogId 被发现为引用主体主键的依赖 Blog.Id 键的外键。 关系在必要时被发现,因为 BlogHeader.BlogId 不能为 null。
  • Blog.BlogHeader 被发现作为参考导航。
  • BlogHeader.Blog 被发现作为参考导航。

重要

使用 C# 可空的引用类型时,如果外键属性为可空,则从依赖主体到主体的导航必须是可空的。 如果外键属性不可为 null,那么导航可以是 null 的或者不是。 在这种情况下, BlogHeader.BlogId 不可为 null, BlogHeader.Blog 也是不可为 null 的。 该 = null!; 构造用于将此标记为有意用于 C# 编译器,因为 EF 通常会设置 Blog 实例,并且对于完全加载的关系,该实例不能为 null。 有关详细信息,请参阅 “使用可为空引用类型 ”。

对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

在上面的示例中,关系配置将启动主体实体类型(Blog)。 就像所有关系一样,以从属实体类型 (BlogHeader) 开始也是完全等效的。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne(e => e.Blog)
        .WithOne(e => e.Header)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

这两个选项没有一个比另一个更好;它们都会导致完全相同的配置。

小窍门

从主体开始一次,然后从依赖主体开始,再重新从依赖项开始,永远不必配置关系两次。 此外,尝试单独配置关系的主体和依赖部分通常不起作用。 选择从一端或另一端配置每个关系,然后只编写一次配置代码。

可选一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int? BlogId { get; set; } // Optional foreign key property
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

这与前面的示例相同,不同之处在于外键属性和指向主体的导航现在可为 null。 这使得关系“可选”,因为依赖 (BlogHeader无法任何 主体()相关,Blog方法是将其外键属性和导航设置为 null

重要

使用 C# 可空引用类型时,如果外键属性可为 null,则从依赖项到主体的导航属性也必须可为 null。 在这种情况下, BlogHeader.BlogId 可为 null,因此 BlogHeader.Blog 也必须为 null。 有关详细信息,请参阅 “使用可为空引用类型 ”。

与以前一样,此关系 是按约定发现的。 对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired(false);
}

主键到主键关系的必需一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

与一对多关系不同,一对一关系的依赖端可能将其主键属性或属性用作外键属性或属性。 这通常被称为 PK-to-PK 关系。 仅当主体和依赖类型具有相同的主键类型并且生成的关系始终是必需的时,这才可能,因为依赖项的主键不能为 null。

任何通过约定无法发现外键的一对一关系都必须进行配置以指示关系的主端和从端。 这通常是通过完成调用HasForeignKey来完成的。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>();
}

小窍门

HasPrincipalKey 也可用于此目的,但这样做不太常见。

如果未在调用 HasForeignKey中指定任何属性,并且主键合适,则将其用作外键。 对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.Id)
        .IsRequired();
}

需要与影子外键关联的一对一关系

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

在某些情况下,你可能不希望在模型中使用外键属性,因为外键是数据库中关系表示方式的详细信息,在完全面向对象的方式使用关系时不需要该属性。 但是,如果要序列化实体(例如通过网络发送),则外键值可能是一种在实体不在对象窗体中时保持关系信息完好无损的有用方法。 因此,出于此目的,将外键属性保留在 .NET 类型中通常是务实的。 外键属性可以是私有的,这通常是一个很好的妥协,以避免公开外键,同时允许其值与实体一起传输。

继上一个示例之后,这个示例删除了依赖实体类型中的外键属性。 但是,不使用主键,而是指示 EF 创建一个名为,类型为BlogIdint

此处要注意的一个重要要点是,正在使用 C# 可为 null 的引用类型,因此,从依赖到主体的导航的可为 null 性用于确定外键属性是否可为 null,这决定了关系是可选的还是必需的。 如果未使用可为 null 的引用类型,则默认情况下,影子外键属性将为 null,使关系默认为可选。 在这种情况下,使用 IsRequired 强制影子外键属性不可为空,并使得关系变为必需。

此关系仍需一些配置来指明主体和依赖端。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId");
}

对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired();
}

具有阴影外键的可选一对一

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

与前面的示例一样,已从依赖实体类型中删除外键属性。 但是,与前面的示例不同,这次外键属性被创建为可为空,因为正在使用 C# 可空引用类型 并且依赖实体类型的导航属性是可空的。 这使得关系是可选的。

如果不使用 C# 可为 null 的引用类型,那么默认情况下,外键属性将被创建为可为 null。 这意味着默认情况下,与自动创建的阴影属性的关系是可选的。

与之前一样,此关系需要一些配置来指示主体和从属关系结束:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId");
}

对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired(false);
}

一对一,无需导航到主体

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

在此示例中,已重新引入外键属性,但已删除依赖对象的导航。

小窍门

只有一个导航关系-一个从依赖主体到主体,一个从主体到依赖主体,但不是两者的关系,称为单向关系。

此关系通过约定发现,因为已发现外键,从而指明依赖方。 对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne()
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

请注意,调用 WithOne 没有参数。 要告诉 EF 从 BlogHeader 没有导航到 Blog,应该这样做。

如果配置从没有导航的实体开始,则必须使用泛型 HasOne<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne<Blog>()
        .WithOne(e => e.Header)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

一对一,无需导航到主体并使用阴影外键

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
}

此示例通过删除依赖对象的外键属性和导航来组合上述两个示例。

与之前一样,此关系需要一些配置来指示主体和从属关系结束:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne()
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired();
}

更完整的配置可以用来显式配置导航和外键名称,并根据需要调用IsRequired()或者IsRequired(false)。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne()
        .HasForeignKey<BlogHeader>("BlogId")
        .IsRequired();
}

一对一,无需导航到依赖项

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

前面的两个示例包含从主体到从属对象的导航,但没有从从属对象到主体的导航。 对于接下来的几个示例,将重新引入依赖对象的导航,同时删除主体上的导航。

按照惯例,EF 会将这视为一对多关系。 要实现一对一,需进行一些最小化的配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne(e => e.Blog)
        .WithOne();
}

请注意,WithOne() 在没有参数的情况下被调用,以表明在该方向上没有导航。

对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<BlogHeader>()
        .HasOne(e => e.Blog)
        .WithOne()
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

如果配置从没有导航的实体开始,则必须使用泛型 HasOne<>() 调用显式指定关系另一端的实体类型。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne<BlogHeader>()
        .WithOne(e => e.Blog)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

无导航的一对一

有时,配置一个不带导航的关系可能会很有用。 这种关系只能通过直接更改外键值来操作。

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

此关系不是通过常规方法发现的,因为没有导航指示这两种类型是相关的。 可以在OnModelCreating中显式配置它。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne<BlogHeader>()
        .WithOne();
}

使用此配置,该 BlogHeader.BlogId 属性仍按约定检测为外键,并且关系为“必需”,因为外键属性不可为 null。 可以通过使外键属性可为 null 来使关系成为“可选”。

此关系的更完整显式配置是:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne<BlogHeader>()
        .WithOne()
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

具有备用键的一对一

到目前为止,所有示例中,依赖对象的外键属性都限于主体上的主键属性。 可以改为将外键限制为不同的属性,该属性随后成为主体实体类型的备用键。 例如:

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public int AlternateId { get; set; } // Alternate key as target of the BlogHeader.BlogId foreign key
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

此关系不是按约定发现的,因为 EF 将始终按约定创建与主键的关系。 可以通过调用OnModelCreating来进行显式HasPrincipalKey配置。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasPrincipalKey<Blog>(e => e.AlternateId);
}

HasPrincipalKey 可以与其他调用结合使用来显式配置导航、外键属性和必需/可选性质。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .HasPrincipalKey<Blog>(e => e.AlternateId)
        .HasForeignKey<BlogHeader>(e => e.BlogId)
        .IsRequired();
}

具有复合外键的一对一

到目前为止,在所有示例中,主体的主键或备用键属性由单个属性组成。 主键或备用键也可以形成多个属性,这些属性称为“复合键”。 当关系的主体具有复合键时,依赖对象的外键也必须是具有相同数量的属性的复合键。 例如:

// Principal (parent)
public class Blog
{
    public int Id1 { get; set; } // Composite key part 1
    public int Id2 { get; set; } // Composite key part 2
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId1 { get; set; } // Required foreign key property part 1
    public int BlogId2 { get; set; } // Required foreign key property part 2
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

此关系是通过常规发现的。 但是,仅当已显式配置复合键时,才会发现它,因为不会自动发现复合键。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasKey(e => new { e.Id1, e.Id2 });
}

重要

如果其任何属性值为 null,则组合外键值将被视为 null 。 一个属性为 null 且另一个非 null 的复合外键不会被视为与具有相同值的主键或备用键的匹配项。 两者都将作为null考虑。

这两个属性HasForeignKeyHasPrincipalKey都可用于显式指定具有多个属性的键。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        nestedBuilder =>
        {
            nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });

            nestedBuilder.HasOne(e => e.Header)
                .WithOne(e => e.Blog)
                .HasPrincipalKey<Blog>(e => new { e.Id1, e.Id2 })
                .HasForeignKey<BlogHeader>(e => new { e.BlogId1, e.BlogId2 })
                .IsRequired();
        });
}

小窍门

在上面的代码中,调用 HasKeyHasOne 已分组到嵌套生成器中。 嵌套生成器无需为同一实体类型多次调用 Entity<>() ,但在功能上等效于多次调用 Entity<>()

需要一对一关系且无级联删除

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public BlogHeader? Header { get; set; } // Reference navigation to dependent
}

// Dependent (child)
public class BlogHeader
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

按照约定,所需的关系配置为 级联删除。 这是因为在删除主体后,数据库中不存在从属关系。 可以将数据库配置为生成错误,通常使应用程序崩溃,而不是自动删除不再存在的依赖行。 这需要一些配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasOne(e => e.Header)
        .WithOne(e => e.Blog)
        .OnDelete(DeleteBehavior.Restrict);
}

自我引用一对一

在前面的所有示例中,主体实体类型与依赖实体类型不同。 这不必是这种情况。 例如,在下面的类型中,每个 Person 类型都与另一种 Person类型(可选)相关。

public class Person
{
    public int Id { get; set; }

    public int? HusbandId { get; set; } // Optional foreign key property
    public Person? Husband { get; set; } // Optional reference navigation to principal
    public Person? Wife { get; set; } // Reference navigation to dependent
}

此关系 由约定发现。 对于约定未发现关系导航、外键或必需/可选性质的情况,可以显式配置这些内容。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasOne(e => e.Husband)
        .WithOne(e => e.Wife)
        .HasForeignKey<Person>(e => e.HusbandId)
        .IsRequired(false);
}

注释

对于一对一自引用关系,由于主体和依赖实体类型相同,因此指定包含外键的类型不明确依赖端。 在这种情况下,HasOne 中指定的导航从从属到主要,而 WithOne 中指定的导航从主要到从属。