Entity Framework — ошибка при добавлении объекта со связанным объектом

#entity-framework #.net-core #entity-framework-core #asp.net-core-3.1

#entity-framework #.net-core #entity-framework-core #asp.net-core-3.1

Вопрос:

У меня есть два объекта:

 public class EntityA
{
    public int? Id { get; set; }
    public string Name { get; set; }
    public EntityB { get; set; }
}

public class EntityB
{
    public int? Id { get; set; }
    public string Version { get; set; }
}
 

У меня уже есть существующие записи в EntityB базе данных. Я хочу добавить новый EntityA со ссылкой на одну из EntityB записей.

 var entityB = _dbContext.EntityB.FirstOrDefault(e => e.Id == 1);

var entityA = new EntityA { Name = "Test", EntityB = entityB };

_dbContext.Add(entityA);
_dbContext.SaveChanges();
 

При выполнении приведенного выше кода я получаю следующую ошибку:

Система.Исключение InvalidOperationException: свойство ‘Id’ для объекта типа ‘EntityB’ является частью ключа и поэтому не может быть изменено или помечено как измененное. Чтобы изменить принципала существующего объекта с идентифицирующим внешним ключом, сначала удалите зависимое и вызовите «SaveChanges», затем свяжите зависимое с новым принципалом.

Мне кажется, что сохранение пытается также добавить EntityB , а не просто ссылку на него. У меня есть связь, указанная как в базе данных, так и в Entity Framework, например, при запросе EntityA , включаю ли я EntityB в select, я также получаю объект, на который ссылается объект (поэтому связь работает).

 modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithOne()
    .HasForeignKey<EntityB>(p => p.Id);
}

modelBuilder.Entity<EntityB>(e =>
{
  e.HasKey(p => p.Id);
}
 

Как я могу сохранить новый EntityA , только со ссылкой на выбранный EntityB , вместо сохранения обоих объектов?

Комментарии:

1. Вы намеревались создать соотношение 1: 1 между EntityB и EntityA ? Ваш код выглядит именно так, но это менее распространенный сценарий, и вопросы, касающиеся конфигурации для 1: 1, обычно явно указаны в этом требовании, это можно легко интерпретировать в любом случае.

2. Вы правы, это должно было быть 1: много.

Ответ №1:

Похоже, вы пытаетесь расширить EntityB с помощью необязательной ссылки 1: 1 на строку в новой таблице EntityA . Вы хотите, чтобы обе записи имели одинаковое значение для Id.

Эту ссылку 1: 1 иногда называют разделением таблицы.
Логически в вашем приложении запись из EntityB и EntityA представляет один и тот же объект бизнес-домена.

Если бы вы просто пытались создать обычную 1 : many связь, то вам следует удалить hasOne().WithOne() поскольку это создает a 1:1 , вы также не пытались бы вернуть FK к свойству Id .

Следующий совет применим только к настройке отношения 1: 1

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

Ваша настройка для этого в основном правильная, за исключением того, что EntityA.Id не может быть обнуляемым, поскольку первичный ключ должен иметь значение.

 public class EntityA
{
    public int Id { get; set; }
    public string Name { get; set; }
    public EntityB { get; set; }
}
 

Если вы хотите, чтобы существовали записи, в EntityA которых НЕТ соответствующей записи EntityB , вам необходимо использовать другой столбец Id в качестве первичного ключа для EntityA или внешнего ключа для EntityB

Затем вам нужно закрыть пробел с EntityA.Id полем, отключив автоматически сгенерированное поведение, чтобы оно принимало Id значение из EntityB :

 modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id).ValueGeneratedNever();
  e.HasOne(p => p.EntityB)
    .WithOne()
    .HasForeignKey<EntityB>(p => p.Id);
}
 

Я бы, вероятно, пошел еще дальше и добавил свойство возвратно-поступательной или обратной навигации EntityB , что позволило бы нам использовать более плавное присвоение стиля вместо использования _dbContext.Add() для добавления записи в базу данных:

 public class EntityB
{
    public int Id { get; set; }
    public string Version { get; set; }
    public virtual EntityA { get; set; }
}
 

С конфигурацией:

 modelBuilder.Entity<EntityA>(e =>
{
    e.HasKey(p => p.Id).ValueGeneratedNever();
    e.HasOne(p => p.EntityB)
     .WithOne(p => p.EntityA)
     .HasForeignKey<EntityB>(p => p.Id);
}
 

Это позволяет добавлять в более свободном стиле:

 var entityB = _dbContext.EntityB.FirstOrDefault(e => e.Id == 1);

entityB.EntityA = new EntityA { Name = "Test" };

_dbContext.SaveChanges();
 

Комментарии:

1. Это должно было быть 1:many . Я удалил .hasOne...withOne инструкцию и добавил EntityBId свойство EntityA с [ForeignKey()] аннотацией, которое, похоже, теперь работает хорошо.

2. В связи с приведенным выше ответом, если я сделаю идентификатор ненулевым, как мне создать новые записи, если база данных отвечает за генерацию новых идентификаторов?

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

4. Не волнуйтесь, EF знает, что делать, идентификатор будет оставаться 0 для новых записей до тех пор, пока не будет вызвано сохранение изменений

Ответ №2:

Это приведет к сбою, потому что вы используете PK EntityA в качестве FK для объекта B, который обеспечивает прямое отношение 1 к 1. Примером этого может быть что-то вроде Order и OrderDetails, которые содержат дополнительные сведения о конкретном заказе. Оба будут использовать «OrderID» в качестве своего PK, а OrderDetails использует его PK для связи с его порядком.

Если вместо этого EntityB больше похож на ссылку OrderType, вы бы не использовали отношение hasOne / WithOne, потому что для этого потребовалось бы, чтобы Order #1 был связан только с OrderType #1. Если бы вы попытались связать OrderType #2 с Order #1, EF попытался бы заменить PK на OrderType, что незаконно.

Обычно для связи между EntityA и EntityB требуется, чтобы столбец EntityBId в таблице EntityA служил в качестве FK. Это может быть свойство в сущности EntityA или оставлено как теневое свойство (рекомендуется, когда EntityA будет иметь свойство навигации EntityB) Используя приведенный выше пример с Order и OrderType, запись Order будет иметь OrderID (PK) и OrderTypeId (FK) для типа заказа, с которым он связан с помощью.

Сопоставление для этого будет: (Теневое свойство)

 modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithMany()
    .HasForeignKey("EntityBId");
}
 

Тип заказа может быть назначен многим заказам, но у нас нет коллекции заказов для OrderType. Мы используем .HasForeignKey("EntityBId") для настройки теневого свойства «EntityBId» в нашей таблице EntityA. В качестве альтернативы, если мы объявим свойство EntityBId в нашем EntityA:

 modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithMany()
    .HasForeignKey(p => p.EntityBId);
}
 

На заметку, свойства навигации должны быть объявлены virtual . Даже если вы не хотите полагаться на отложенную загрузку (рекомендуется), это помогает гарантировать, что прокси EF для отслеживания изменений будут полностью поддерживаться, а отложенная загрузка, как правило, является лучшим условием для выполнения во время выполнения, чем выбрасывание NullReferenceExceptions .