#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 .