一对一关系

当一个实体与最多一个其他实体关联时,将使用一对一关系。 例如,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 不可为空。 这会使关系成为“必需”关系,因为每个依赖实体 (BlogHeader) 必须与某个主体实体 (Blog) 相关,而其外键属性必须设置为某个值
  • 这两个实体都有指向关系另一端的相关实体的导航。

注意

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

提示

具有两个导航的关系(一个是从依赖实体到主体实体,一个是从主体实体到依赖实体)称为双向关系。

此关系按约定发现。 即:

  • Blog 作为关系中的主体实体被发现,BlogHeader 作为依赖实体被发现。
  • BlogHeader.BlogId 作为引用主体实体的 Blog.Id 主键的依赖实体的外键被发现。 由于 BlogHeader.BlogId 不可为空,所以发现这一关系是必需的。
  • Blog.BlogHeader 作为引用导航被发现。
  • BlogHeader.Blog 作为引用导航被发现。

重要

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

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

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
}

这与上一个示例相同,只不过外键属性和到主体实体的导航现在可为空。 这会使关系成为“可选”关系,因为依赖实体 (BlogHeader) 不能通过将其外键属性和导航设置为 null 来与任何主体 (Blog) 相关。

重要

使用 C# 可为空引用类型时,如果外键属性可为空,则从依赖实体到主体实体的导航属性必须可为空。 在本例中,BlogHeader.BlogId 不可为空,因此 BlogHeader.Blog 也不得为空。 有关详细信息,请参阅使用可为空引用类型

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

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 到 PK 关系。 仅当主体类型和依赖类型具有相同的主键类型,并且始终需要生成的关系时才有可能,因为依赖类型的主键不可为空。

必须配置未按约定发现外键的任何一对一关系,以指示关系的主体端和依赖端。 这通常是使用对 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# 可为空引用类型,因此从依赖实体到主体实体的导航的可为空性用于确定外键属性是否可为空,进而确定关系是可选的还是必需的。 如果未使用可为空引用类型,则默认情况下,阴影外键属性将为空,使关系默认为可选。 在这种情况下,使用 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# 可为空引用类型,则默认情况下,外键属性将创建为“可为空”。 这意味着,与自动创建的阴影属性的关系默认可选。

如前所述,此关系需要一些配置来指示主体端和依赖端:

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 没有从 BlogHeaderBlog 的导航的方式。

如果从没有导航的实体开始配置,则必须使用泛型 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 属性仍被检测为外键,并且关系是“必需的”,因为外键属性不可为空。 通过将外键属性设为“可为空”,可以使关系成为“可选”关系。

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

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 始终按照约定创建与主键的关系。 可以使用对 HasPrincipalKey 的调用在 OnModelCreating 中显式配置它。 例如:

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

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 点为单位指定的导航。