Связи, свойства навигации и внешние ключи

В этой статье приводится обзор того, как Entity Framework управляет связями между сущностями. Он также дает некоторые рекомендации по сопоставлению и манипулированию отношениями.

Связи в EF

В реляционных базах данных связи (также называемые ассоциациями) между таблицами определяются через внешние ключи. Внешний ключ (FK) — это столбец или сочетание столбцов, которые используются для установления и принудительного применения связи между данными в двух таблицах. Как правило, существуют три типа отношений: "один к одному", "один ко многим" и "многие ко многим". В отношении "один ко многим" внешний ключ определяется в таблице, представляющей множество в конце отношения. Связь "многие ко многим" предусматривает определение третьей таблицы (называемой таблицей связи или таблицей соединений), первичный ключ которой состоит из внешних ключей из обеих связанных таблиц. В связи "один к одному" первичный ключ выступает дополнительно в качестве внешнего ключа, и для любой таблицы нет отдельного внешнего ключевого столбца.

На следующем рисунке показаны две таблицы, участвующие в отношениях типа "один ко многим". Таблица "Курс " является зависимой таблицей, так как она содержит столбец DepartmentID , который связывает ее с таблицей Отдела .

Таблицы отделов и курсов

В Entity Framework сущность может быть связана с другими сущностями через ассоциацию или отношение. Каждая связь содержит два конца, описывающие тип сущности и кратность типа (один, ноль или один, или много) для двух сущностей в этой связи. Связь может управляться референциальным ограничением, описывающим, какая сторона в отношениях играет главную роль и какая — зависимую.

Свойства навигации предоставляют способ навигации между двумя типами сущностей. Каждый объект может иметь свойство навигации для каждой связи, в которой она участвует. Свойства навигации позволяют перемещаться по связям и управлять ими в обоих направлениях, возвращая объект-ссылку (если кратность равна одному или нулю) или коллекцию (если кратность равна многим). Вы также можете выбрать односторонняя навигация, в этом случае вы определяете свойство навигации только для одного из типов, участвующих в связи, а не для обоих.

Рекомендуется включить свойства в модель, которые сопоставляются с внешними ключами в базе данных. Если включены свойства внешнего ключа, можно создать или изменить связь, изменив значение внешнего ключа для зависимого объекта. Такая связь называется внешней ассоциацией ключей. Использование внешних ключей еще более важно при работе с отключенными сущностями. Обратите внимание, что при работе с отношениями 1-к-1 или 1-к-0..1 нет отдельного столбца для внешнего ключа: свойство первичного ключа используется в качестве внешнего ключа и всегда включается в модель.

Если столбцы внешнего ключа не включены в модель, информация об ассоциации обрабатывается как независимый объект. Связи отслеживаются с помощью ссылок на объекты вместо свойств внешнего ключа. Этот тип ассоциации называется независимой ассоциацией. Наиболее распространенным способом изменения независимой ассоциации является изменение свойств навигации, создаваемых для каждой сущности, которая участвует в ассоциации.

Вы можете использовать одно или оба типа связей в модели. Однако, если у вас есть простая связь "многие ко многим", реализуемая через таблицу связей, содержащую только внешние ключи, EF будет использовать независимую ассоциацию для управления такой связью "многие ко многим".   

На следующем рисунке показана концептуальная модель, созданная с помощью конструктора Entity Framework. Модель содержит две сущности, участвующие в связи "один ко многим". Обе сущности имеют навигационные свойства. Курс является зависимой сущностью и имеет свойство внешнего ключа DepartmentID .

Таблицы отдела и курса со свойствами навигации

В следующем фрагменте кода показана та же модель, которая была создана с помощью 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;
    

    Замечание

    Если ссылка находится в добавленном состоянии (в этом примере объект курса), свойство навигации ссылок не будет синхронизировано со значениями ключей нового объекта до вызова SaveChanges. Синхронизация не происходит, так как контекст объекта не содержит постоянные ключи для добавленных объектов, пока они не будут сохранены. Если вы должны иметь новые объекты полностью синхронизированы сразу после установки связи, используйте один из следующих методов.*

  • При назначении нового объекта свойству навигации. Следующий код создает связь между курсом и department. Если объекты присоединены к контексту, элемент course также добавляется в коллекцию department.Courses, и соответствующему свойству внешнего ключа в объекте course присваивается значение ключевого свойства отдела.

    course.Department = department;
    
  • Чтобы удалить связь, задайте для свойства навигации значение null. Если вы работаете с Entity Framework, основанной на .NET 4.0, необходимо загрузить связанный конец, прежде чем задать значение NULL. Рассмотрим пример.

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

    Начиная с Entity Framework 5.0, основанного на .NET 4.5, вы можете установить отношение в null без загрузки связанного объекта. Текущее значение можно также установить в NULL с помощью следующего метода.

    context.Entry(course).Reference(c => c.Department).CurrentValue = null;
    
  • При удалении или добавлении объекта в коллекцию сущностей. Например, можно добавить объект типа Course в коллекцию department.Courses . Эта операция создает связь между определенным курсом и конкретным department. Если объекты присоединены к контексту, ссылка на отдел и свойство внешнего ключа объекта курса будут заданы соответствующим departmentобразом.

    department.Courses.Add(newCourse);
    
  • С помощью ChangeRelationshipState метода можно изменить состояние указанной связи между двумя объектами сущности. Этот метод чаще всего используется при работе с приложениями уровня N и независимой ассоциацией (ее нельзя использовать с ассоциацией внешнего ключа). Кроме того, чтобы использовать этот метод, необходимо переключиться на ObjectContext, как показано в приведенном ниже примере.
    В следующем примере существует отношение "многие ко многим" между преподавателями и курсами. 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);
    

Синхронизация изменений между внешними ключами и свойствами навигации

При изменении связи объектов, присоединенных к контексту, с помощью одного из описанных выше методов Entity Framework необходимо сохранить внешние ключи, ссылки и коллекции в синхронизации. Entity Framework автоматически управляет этой синхронизацией (также называемой исправлением связи) для сущностей 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
  • Выполнение запроса LINQ для DbSet

В Entity Framework обычно используются свойства навигации для загрузки сущностей, связанных с возвращаемой сущностью определенной ассоциацией. Дополнительные сведения см. в разделе "Загрузка связанных объектов".

Замечание

При ассоциации с внешним ключом, при загрузке конечного элемента связанной части зависимого объекта, связанный объект будет загружен на основе значения внешнего ключа зависимого, который в настоящее время находится в памяти:

    // 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();

В независимом объединении связанная часть зависимого объекта опрашивается на основании значения внешнего ключа, который в настоящее время присутствует в базе данных. Однако если связь была изменена, а ссылочное свойство зависимых объектов указывает на другой основной объект, загруженный в контексте объекта, Entity Framework попытается создать связь, так как она определена на клиенте.

Управление конкурентностью

В внешних и независимых сопоставлениях проверки параллелизма основаны на ключах сущностей и других свойствах сущностей, определенных в модели. При использовании конструктора EF для создания модели задайте для атрибута ConcurrencyModeфиксированное значение, чтобы указать, что свойство должно быть проверено для параллелизма. При использовании code First для определения модели используйте заметку ConcurrencyCheck о свойствах, которые необходимо проверить для параллелизма. При работе с кодом First можно также использовать заметку TimeStamp , чтобы указать, что свойство должно быть проверено для параллелизма. В данном классе может быть только одно свойство метки времени. Код сначала сопоставляет это свойство с полем в базе данных, не допускающим значения NULL.

Рекомендуется всегда использовать ассоциацию внешнего ключа при работе с сущностями, участвующими в проверке параллелизма и разрешении.

Дополнительные сведения см. в разделе "Обработка конфликтов параллелизма".

Работа с перекрывающимися ключами

Перекрывающиеся ключи — это составные ключи, в которых некоторые свойства ключа также являются частью другого ключа в сущности. Вы не можете иметь перекрывающийся ключ в независимой ассоциации. Чтобы изменить связь внешнего ключа, которая включает перекрывающиеся ключи, рекомендуется изменить значения внешнего ключа вместо использования ссылок на объекты.