Возникают исключения при попытке управления данными через Entity Framework
Есть класс контекста данных ApplicationContext.cs
:
internal class ApplicationContext : DbContext
{
public DbSet<Contact>? Contacts { get; set; }
public DbSet<PhoneType>? PhoneTypes { get; set; }
public DbSet<ContactInfo>? ContactsInfo { get; set; }
public ApplicationContext() : base("Data Source=XXX;Initial Catalog=ContactsNotebookDB;Integrated Security=True;TrustServerCertificate=True")
{
Database.CreateIfNotExists();
}
}
Детализация записей:
Contact.cs
:
internal class Contact : IDatabase
{
public required int ID { get; set; }
public required string PhoneNumber { get; set; }
public int? PhoneTypeID { get; set; }
public required int ContactInfoID { get; set; }
}
ContactInfo.cs
:
[Table("ContactsInfo")] // обязательное явное указание названия таблицы
(по умолчанию EF читает как "ContactInfoes")
internal class ContactInfo : IDatabase
{
public required int ID { get; set; }
public required string Name { get; set; }
public required string Surname { get; set; }
public string? Patronymic { get; set; }
public required char Sex { get; set; }
}
Когда я пытаюсь сохранить данные в коде одного из View
, то возникает исключение:
using ApplicationContext ApplicationContext = new();
int ContactsInfoMaxID = ApplicationContext.ContactsInfo.Count();
int ContactsMaxID = ApplicationContext.Contacts.Count();
ContactInfo ContactInfo = new()
{
ID = ContactsInfoMaxID + 1,
Name = NameTextBox.Text,
Surname = SurnameTextBox.Text,
Patronymic = Patronymic,
Sex = SexComboBox.Text[0]
};
Contact Contact = new()
{
ID = ContactsMaxID + 1,
PhoneNumber = PhoneNumberTextBox.Text,
PhoneTypeID = PhoneTypeComboBox.SelectedIndex,
ContactInfoID = ContactInfo.ID,
};
ApplicationContext.ContactsInfo.Add(ContactInfo);
ApplicationContext.Contacts.Add(Contact);
ApplicationContext.SaveChanges(); // тут возникает исключение при сохранении
Строка ApplicationContext.SaveChanges();
вызывает следующее исключение:
System.Data.Entity.Infrastructure.DbUpdateException: "An error occurred while updating the entries. See the inner exception for details."
И 2 внутренних исключения:
UpdateException: An error occurred while updating the entries. See the inner exception for details.
SqlException: Не удалось вставить значение NULL в столбец "ID", таблицы "ContactsNotebookDB.dbo.Contacts"; в столбце запрещены значения NULL. Ошибка в INSERT.
Выполнение данной инструкции было прервано.
Обновление проблемы:
При попытке удаление записи из БД также возникает исключение.
Фрагмент кода ViewModel
с методом, который пытается удалить данные:
/// <summary>
/// Команда удаления контакта
/// </summary>
/// <param name="parameter"></param>
private void DeleteContact(object parameter)
{
MessageBoxResult DeleteContactMessageBoxResult = MessageBox.Show($"Внимание, вы действительно хотите удалить следующий контакт:\n\n{PhoneNumber}\n{Surname} {Name}{(Patronymic == "—" ? null : " " + Patronymic)}?", "Удаление контакта", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (DeleteContactMessageBoxResult == MessageBoxResult.Yes)
{
using ApplicationContext ApplicationContext = new();
bool IsContactInUse = ApplicationContext.Contacts.Any(Contact => Contact.ContactInfoID == ID);
if (!IsContactInUse)
{
ApplicationContext.ContactsInfo?.Remove(ContactInfo);
}
else
{
Console.WriteLine("Текущий используется в Contact и не может быть удален.");
}
ApplicationContext.Contacts?.Remove(Contact);
ApplicationContext.SaveChanges();
}
}
Строка ApplicationContext.Contacts?.Remove(Contact);
вызывает исключение:
System.InvalidOperationException: "The object cannot be deleted because it was not found in the ObjectStateManager."
Дополнительная информация:
- Версия
Entity Framework
— 6.5.1; - Я уверен в правильности записей, вносимых в БД, потому что во время проверки через отладку записи вносятся в
ApplicationContext
:
Уже странно, что ContactsInfo
возвращает только 1 новую запись, в то время как Contacts
возвращает все записи + новую запись (она, кстати, почему-то помещается наверх таблицы).
Даже если я явно задам Null-данные
, запись по прежнему не будет вноситься в БД, отображая то же исключение.
P. S. Я увидел 0 в добавляемой записи Contacts
(атрибут PhoneTypeID
), исправил его на 1, но на ошибке это не отразилось.
Выражение
ApplicationContext.ContactsInfo.Sql
в отладке возвращает значение:SELECT [Extent1].[ID] AS [ID], [Extent1].[Name] AS [Name], [Extent1].[Surname] AS [Surname], [Extent1].[Patronymic] AS [Patronymic] FROM [dbo].[ContactsInfo] AS [Extent1]
То есть тут почему-то отсутствует столбец Sex
из таблицы БД.
Я пересоздавал БД, создавал дубликат БД без записей, без связей, у которого не было первичных ключей и обязательных полей, но в таком случае вылетает другое исключение:
System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: "Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions."
И внутреннее:
OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.
- Струткура таблиц в БД MS SQL:
ContactsInfo
:
Contacts
:
Если попробовать добавить запись не в
Contacts
, а вContactsInfo
и попытаться сохранить изменения, то возникает то же исключение, но с другим внутренним исключением:SqlException: Не удалось вставить значение NULL в столбец "Sex", таблицы "ContactsNotebookDB.dbo.ContactsInfo"; в столбце запрещены значения NULL. Ошибка в INSERT. Выполнение данной инструкции было прервано.
А, ну из очевидного, конечно,
ApplicationContext
получает данные из БД, ведь он возвращает верные значения при обращении к нему (кол-во таблиц и их записи), поэтому проблема не в подключении.
Обновлено:
- Непонятно почему строка
ApplicationContext.Contacts?.Remove(Contact);
вызывает исключение, ведь данные из БД по контактам загрузились вApplicationContext
(хотя проверка в отладке работает 50/50: либо отображает загруженныеContacts
, либо пишет, что их кол-во — 0).
Ответы (1 шт):
Предложение @Alexander Petrov в комментариях:
Короче, убирайте строки int ContactsInfoMaxID = ApplicationContext.ContactsInfo.Count(); int ContactsMaxID = ApplicationContext.Contacts.Count();. Убирайте ID = ContactsInfoMaxID + 1, и ID = ContactsMaxID + 1, (оставьте айдишники неинициализированными, БД сама их задаст).
было прямо в яблочко! Вот только я воспринимал это как совет, но никак не думал, что это будет единственным способом решения данной проблемы. Кстати, применяя его в первый раз почему-то мне не удавалось решить проблему.
Как ни странно, начну с починки возможности удаления записи из БД, ведь она происходит легче.
Для этого нужно просто прикрепить удаляемую запись с помощью метода Attach()
перед удалением с помощью Remove()
.
Например:
ApplicationContext.Contacts.Attach(Contact); // прикрепляем удаляемый объект к контексту
ApplicationContext.Contacts.Remove(Contact);
await ApplicationContext.SaveChangesAsync();
Но Attach()
нужен лишь в том случае, если:
- объект был получен из другого источника (например, из кэша, с фронтенда или другого контекста);
- объект был создан вручную и необходимо удалить или обновить его в базе данных.
То есть если объект был получен из базы данных через текущий контекст (через методы вроде Find
, FirstOrDefault
, LINQ-запросы
и т. д.), то в таком случае объект уже отслеживается и использование метода Attach()
не требуется.
И всё. Этого достаточно для того, чтобы записи удалялись. И никаких "танцев с бубном" по типу изменения первичного ключа на автоинкрементный. Я не говорю, что автоинкрементный ключ — плохо. Я лишь говорю то, что Remove()
хотя бы предоставляет свободу выбора.
Сразу говорю, ответ взят отсюда: https://stackoverflow.com/questions/7791149/the-object-cannot-be-deleted-because-it-was-not-found-in-the-objectstatemanager.
Чего не скажешь о добавлении записи в БД с помощью метода Add()
...
Для того, чтобы можно было вносить новые записи в БД нужно... нет, не изменить код, а изменить архитектуру в БД.
А именно указать ключу ID
то, что он автоинкрементный. И всё!
То есть в проекте таблицы в MS SQL
изменить параметр поля ID
следующим образом:
Итоговая версия кода:
ContactInfo ContactInfo = new()
{
Name = NameTextBox.Text,
Surname = SurnameTextBox.Text,
Patronymic = Patronymic,
Sex = SexComboBox.Text/*[0]*/
};
Contact Contact = new()
{
PhoneNumber = PhoneNumberTextBox.Text,
PhoneTypeID = PhoneTypeID,
ContactInfoID = ContactInfo.ID
};
using ApplicationContext ApplicationContext = new();
if (ContactViewModel != null) // если редактируем запись
{
Contact? DBContact = ApplicationContext.Contacts?.FirstOrDefault(Contact => Contact.ID == ContactViewModel.ID);
ContactInfo? DBContactInfo = ApplicationContext.ContactsInfo?.FirstOrDefault(Contact => Contact.ID == ContactViewModel.ContactInfo.ID);
if (DBContactInfo != null)
{
DBContactInfo.Name = ContactInfo.Name;
DBContactInfo.Surname = ContactInfo.Surname;
DBContactInfo.Patronymic = ContactInfo.Patronymic;
DBContactInfo.Sex = ContactInfo.Sex;
}
if (DBContact != null)
{
DBContact.PhoneNumber = Contact.PhoneNumber;
DBContact.PhoneTypeID = Contact.PhoneTypeID;
}
}
else // если создаём новую запись
{
ApplicationContext.ContactsInfo?.Add(ContactInfo);
await ApplicationContext.SaveChangesAsync();
Contact.ContactInfoID = ContactInfo.ID;
ApplicationContext.Contacts?.Add(Contact);
}
await ApplicationContext.SaveChangesAsync();
Обновлено:
Важно! Обратите внимание на то, что если у вас есть подчинённая сущность (у меня в данном случае свойство ContactInfoID
у Contact
, зависящее от ID
у ContactInfo
), то перед добавлением новых записей в БД нужно сохранять сущности поочерёдно:
ApplicationContext.ContactsInfo?.Add(ContactInfo);
await ApplicationContext.SaveChangesAsync();
Contact.ContactInfoID = ContactInfo.ID;
ApplicationContext.Contacts?.Add(Contact);
await ApplicationContext.SaveChangesAsync();
Потому-что в момент инициализации ID
у ContactInfo
равно 0
— ему присвоется ID
только после добавления в БД! Особенно явно это видно если в БД выставлены ограничения по внешним ключам: