关系、导航属性和外键

本文概述实体框架如何管理实体之间的关系, 还提供有关如何映射和操作关系的一些指导。

EF 中的关系

在关系数据库中,表之间的关系(也称为关联)是通过外键定义的。 外键 (FK) 是用于建立和加强两个表数据之间的链接的一列或多列。 通常有三种类型的关系:一对一、一对多和多对多。 在一对多关系中,外键在表示关系的“多”端的表上定义。 多对多关系涉及定义第三个表(称为联接表),其主键由来自两个相关表的外键组成。 在一对一关系中,主键还充当外键,并且两个表都没有单独的外键列。

下图显示了参与一对多关系的两个表。 Course 表是依赖表,因为它包含将其链接到 Department 表的 DepartmentID 列

Department and Course tables

在实体框架中,实体可以通过关联或关系与其他实体相关联。 每个关系包含两个端,用于说明该关系中两个实体的实体类型和类型重数(一个、零或一个、多个)。 关系可由引用约束控制,该引用约束描述了关系中的哪一端是主要角色以及哪一端是从属角色。

导航属性提供了一种在两个实体类型之间导航关联的方法。 针对对象参与到其中的每个关系,各对象均可以具有导航属性。 使用导航属性,可以在两个方向上导航和管理关系,在重数为一或者零或一的情况下,返回引用对象,在重数为多个的情况下,返回一个集合。 也可以选择使用单向导航,这种情况下可以仅对参与关系的其中一个类型定义导航属性,而不是同时对两个类型定义。

建议在模型中包含映射到数据库中外键的属性。 加入了外键属性,您就可以通过修改依赖对象的外键值来创建或更改关系。 此类关联称为外键关联。 使用断开连接的实体时,更需要使用外键。 请注意,在使用 1 对 1 或 1 对 0..1 关系时,没有单独的外键列,主键属性充当外键,并且始终包含在模型中。

如果模型中未包含外键列,则关联信息将作为独立对象进行管理。 通过对象引用(而不是外键属性)跟踪关系。 此类关联称为独立关联。 修改独立关联的最常用方法就是修改为参与到关联中的每个实体生成的导航属性

可以在您的模型中选择使用一种或两种类型的关联。 但是,如果有一个纯多对多关系,而该关系由只包含外键的联接表连接,那么 EF 将使用独立关联来管理这种多对多关系。   

下图显示了使用 Entity Framework Designer 创建的概念模型。 该模型包含两个参与一对多关系的实体。 这两个实体都有导航属性。 Course 是依赖实体,并定义了 DepartmentID 外键属性

Department and Course tables with navigation properties

以下代码片段显示了使用 Code First 创建的相同模型。

public class Course
{
  public int CourseID { get; set; }
  public string Title { get; set; }
  public int Credits { get; set; }
  public int DepartmentID { get; set; }
  public virtual Department Department { get; set; }
}

public class Department
{
   public Department()
   {
     this.Courses = new HashSet<Course>();
   }  
   public int DepartmentID { get; set; }
   public string Name { get; set; }
   public decimal Budget { get; set; }
   public DateTime StartDate { get; set; }
   public int? Administrator {get ; set; }
   public virtual ICollection<Course> Courses { get; set; }
}

配置或映射关系

本页的其余部分介绍如何使用关系访问和操作数据。 有关在模型中设置关系的信息,请参阅以下页面。

创建和修改关系

在外键关联中,更改关系后,具有 EntityState.Unchanged 状态的依赖对象的状态将更改为 EntityState.Modified。 在独立关系中,更改关系不会更新依赖对象的状态。

下面的示例演示如何使用外键属性和导航属性关联相关对象。 通过外键关联,可以使用任一方法更改、创建或修改关系。 使用独立关联,则不能使用外键属性。

  • 通过为外键属性赋予新值,如以下示例所示。

    course.DepartmentID = newCourse.DepartmentID;
    
  • 以下代码通过将外键设置为 null 来删除关系。 请注意,外键属性必须可为 null。

    course.DepartmentID = null;
    

    注意

    如果引用处于已添加状态(在本例中为 course 对象),则引用导航属性将不与新对象的键值同步,直至调用 SaveChanges。 由于对象上下文在键值保存前不包含已添加对象的永久键,因此不发生同步。 如果必须在设置关系后对新对象进行完全同步,请使用以下方法之一。*

  • 通过将一个新对象分配给导航属性。 下面的代码在 course 与 department 之间创建关系。 如果将对象附加到上下文,则 course 也将添加到 department.Courses 集合,且 course 对象的相应外键属性将设置为 department 的键属性值:

    course.Department = department;
    
  • 若要删除关系,请将导航属性设置为 null。 如果使用的是基于 .NET 4.0 的 实体框架,则需要先加载相关端,然后才能将其设置为 null。 例如:

    context.Entry(course).Reference(c => c.Department).Load();
    course.Department = null;
    

    从 实体框架 5.0(基于 .NET 4.5)开始,可将关系设置为 null,而无需加载相关端。 还可使用下列方法将当前值设置为 null。

    context.Entry(course).Reference(c => c.Department).CurrentValue = null;
    
  • 通过在实体集合中删除或添加对象。 例如,可将类型为 Course 的对象添加到 department.Courses 集合。 此操作在特定 course 与特定 department 之间创建关系。 如果将对象附加到上下文,则 course 对象的 department 引用和外键属性将设置为相应的 department

    department.Courses.Add(newCourse);
    
  • 使用 ChangeRelationshipState 方法可更改两个实体对象之间的指定关系的状态。 使用 N 层应用程序和独立关联时,此方法最为常用(不能用于外键关联)。 此外,若要使用此方法,必须下拉到 ObjectContext,如以下示例所示。
    在下面的示例中,Instructor和 Course 之间存在多对多关系。 调用 ChangeRelationshipState 方法并传递 EntityState.Added 参数,让 SchoolContext 知道已在两个对象之间添加关系:

    
    ((IObjectContextAdapter)context).ObjectContext.
      ObjectStateManager.
      ChangeRelationshipState(course, instructor, c => c.Instructor, EntityState.Added);
    

    请注意,若要更新(而不仅仅是添加)关系,必须在添加新关系后删除旧关系:

    ((IObjectContextAdapter)context).ObjectContext.
      ObjectStateManager.
      ChangeRelationshipState(course, oldInstructor, c => c.Instructor, EntityState.Deleted);
    

同步外键和导航属性之间的更改

使用上述某个方法更改附加到上下文的对象的关系时,实体框架需要同步更新外键、引用和集合。实体框架自动为含代理的 POCO 实体管理此同步。 有关详细信息,请参阅使用代理

如果使用的 POCO 实体不含代理,则必须确保调用 DetectChanges 方法来同步上下文中的相关对象。 请注意,以下 API 会自动触发 DetectChanges 调用

  • DbSet.Add
  • DbSet.AddRange
  • DbSet.Remove
  • DbSet.RemoveRange
  • DbSet.Find
  • DbSet.Local
  • DbContext.SaveChanges
  • DbSet.Attach
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries
  • DbSet 执行 LINQ 查询

在实体框架中,通常使用导航属性来加载与定义的关联返回的实体相关的实体。 有关详细信息,请参阅加载相关对象

注意

在外键关联中,加载依赖对象的相关端时,将会基于当前位于内存中的依赖对象的外键值加载相关对象:

    // Get the course where currently DepartmentID = 2.
    Course course = context.Courses.First(c => c.DepartmentID == 2);

    // Use DepartmentID foreign key property
    // to change the association.
    course.DepartmentID = 3;

    // Load the related Department where DepartmentID = 3
    context.Entry(course).Reference(c => c.Department).Load();

在独立关联中,基于当前数据库中的外键值查询依赖对象的相关端。 但是,如果关系已修改,且依赖对象的引用属性指向对象上下文中加载的不同主体对象,实体框架将尝试按照客户端上的定义创建关系。

管理并发

在外键和独立关联中,并发检查基于实体键和模型中定义的其他实体属性。 使用 EF Designer 创建模型时,将 ConcurrencyMode 属性设置为“固定”,以指定应检查属性的并发性。 使用 Code First 定义模型时,对要检查其并发性的属性使用 ConcurrencyCheck 注释。 使用 Code First 时,还可使用 TimeStamp 注释来指定应检查该属性的并发性。 给定的类中只能有一个时间戳属性。 Code First 将此属性映射到数据库中不可为 null 的字段。

建议在使用参与并发检查和解决的实体时,始终使用外键关联。

有关详细信息,请参阅处理并发冲突

使用重叠键

重叠键是指键中某些属性亦是实体中其他键的一部分的那些组合键。 在独立关联中不能包含重叠键。 若要更改包含重叠键的外键关联,我们建议您修改外键值而不要使用对象引用。