#c_sharp #entity_framework
Проблема с добавление данных в БД с ипользованием Entity Framework, никак не могу понять причину, почему после добавления в БД записи с неуникальным ключом, все последующие записи отбрасываются с ошибкой. Т.е. после ошибки (исключение "System.Data.Entity.Infrastructure.DbUpdateException" в EntityFramework.dll): Ошибка: Нарушено "PK_PassportSystem_SZI_1" ограничения PRIMARY KEY. Не удается вставить повторяющийся ключ в объект "dbo.PassportSystem_SZI". Повторяющееся значение ключа: (3598, 20, -).Выполнение данной инструкции было прервано. Вставить корректную запись не удается (исключение "System.Data.Entity.Infrastructure.DbUpdateException" в EntityFramework.dll), хотя строка имеет уже уникальные значения ключа: Ошибка: Нарушено "PK_PassportSystem_SZI_1" ограничения PRIMARY KEY. Не удается вставить повторяющийся ключ в объект "dbo.PassportSystem_SZI". Повторяющееся значение ключа: (3598, 20, -).Выполнение данной инструкции было прервано. Контекст создается в конструкторе: public class Class_EntityConnection { public static LicMngrEntities Db { get; set; } static Class_EntityConnection() { LicMngrEntities _db = new LicMngrEntities(); Db = _db; } Метод добавления в БД заполненного класса: public static int Insert(TEntity entity) where TEntity : class { Db.Database.Log = (s => System.Diagnostics.Debug.WriteLine(s)); try { Db.Entry(entity).State = EntityState.Added; Db.SaveChanges(); return 0; } catch (Exception ex) { MessageBox.Show("Ошибка при добавлении записи в БД" + ex.Message); return -1; } } Метод создания и заполнения класса и передача его на запись: PassportSystem_SupplyItem passSystemSuppItem = new PassportSystem_SupplyItem { id_SupItem_PassSys_SupItem = Convert.ToInt32(cbSelectSuppItem.SelectedValue), idSystem_PassSys_SupItem = IdSystem, serialNumberPassSys_SupItem = tbSerialNumb.Text, countPassSys_SupItem = Convert.ToInt32(tbCount.Text), dateEndPassSys_SupItem = Convert.ToDateTime(dpDateEnd.Text).Date, dateStartPassSys_SupItem = Convert.ToDateTime(dpDateStart.Text).Date, dateSupplyPassSys_SupItem = Convert.ToDateTime(dpDateSupply.Text).Date, notePassSys_SupItem = tbNote.Text }; Class_EntityConnection.Insert(passSystemSuppItem); Лог успешной транзакции: Запущенная транзакция в 17.07.2018 18:30:39 +03:00 INSERT [dbo].[PassportSystem_SZI]([idSZI_PassSys_SZI], [idSystem_PassSys_SZI], [serialNumberPassSys_SZI], [countPassSys_SZI], [dateStartPassSys_SZI], [dateEndPassSys_SZI], [dateSupplyPassSys_SZI], [notePassSys_SZI])VALUES (@0, @1, @2, @3, @4, @5, @6, @7) -- @0: '3598' (Type = String, Size = 20) -- @1: '20' (Type = Int32) -- @2: '9999' (Type = String, Size = 50) -- @3: '9' (Type = Int32) -- @4: '29.06.2018 0:00:00' (Type = DateTime2) -- @5: '29.06.2018 0:00:00' (Type = DateTime2) -- @6: '29.06.2018 0:00:00' (Type = DateTime2) -- @7: '' (Type = String, Size = 255) -- Выполнение в 17.07.2018 18:30:39 +03:00 -- Выполнено за 15 мс. Результат: 1 Лог ошибочной (специально введены некорректные значения ключа) Запущенная транзакция в 17.07.2018 18:30:53 +03:00 INSERT [dbo].[PassportSystem_SZI]([idSZI_PassSys_SZI], [idSystem_PassSys_SZI], [serialNumberPassSys_SZI], [countPassSys_SZI], [dateStartPassSys_SZI], [dateEndPassSys_SZI], [dateSupplyPassSys_SZI], [notePassSys_SZI])VALUES (@0, @1, @2, @3, @4, @5, @6, @7) -- @0: '3598' (Type = String, Size = 20) -- @1: '20' (Type = Int32) -- @2: '-' (Type = String, Size = 50) -- @3: '9' (Type = Int32) -- @4: '29.06.2018 0:00:00' (Type = DateTime2) -- @5: '29.06.2018 0:00:00' (Type = DateTime2) -- @6: '29.06.2018 0:00:00' (Type = DateTime2) -- @7: '' (Type = String, Size = 255) -- Выполнение в 17.07.2018 18:30:53 +03:00 -- Сбой через 20 мс. Ошибка: Нарушено "PK_PassportSystem_SZI_1" ограничения PRIMARY KEY. Не удается вставить повторяющийся ключ в объект "dbo.PassportSystem_SZI". Повторяющееся значение ключа: (3598, 20, -).Выполнение данной инструкции было прервано. Последующие транзакции в сеансе выдают аналогичный лог.
Ответы
Ответ 1
Дело в том, что DbContext реализует паттерн Unit of Work (Единица Работы). Паттерн используется для представления бизнес-транзакций на уровне предметной области. Единица работы копит изменения, которые должны быть сделаны в базе данных и затем сохраняет их все вместе. В DbContext для регистрации добавления/удаления применяют методы DbSet.Add и DbSet.Delete, а для сохранения — метод SaveChanges. Здесь и кроется корень проблемы. Если при сохранении возникает исключение, все объекты помеченные на вставку, обновление или удаление, таковыми и остаются. При следующем сохранении DbContext попробует снова сохранить их в базе данных. Теперь о том, как исправить ситуацию. Во-первых, надо понимать, что время жизни DbContext должно быть коротким, чтобы ошибка в одной записи не приводила к ошибкам в других записях. К счастью, расходы на создание объектов DbContext невелики: Entity Framework при первом создании DbContext строит схему отображения базы данных на объекты и сохраняет её, поэтому все последующие контексты создаются очень быстро. Вставлять запись лучше через метод Add: public class MyDbContext : DbContext { public virtual DbSetSupplyItems { get; set; } } … public void Add(IEnumerable items) { foreach (var item in items) { using (var dbContext = new MyDbContext()) { dbContext.Add(item); dbContext.SaveChanges(); } } } В вашем случае DbContext надо создавать для каждой записи. Но есть и другие способы. Если записей не очень много, в пределах нескольких сотен, вы можете прочитать их в память, и уже в памяти определить, какие их них новые, а какие существующие. Если записей много, ту же самую операцию можно сделать постраничной. public void Add(IEnumerable items) { using (var dbContext = new MyDbContext()) { var oldItems = dbContext.SupplyItems.ToList(); var itemsToInsert = items.Except(oldItems); var itemsToUpdate = from oldItem in oldItems join newItem in items on newItem.Id equals item.Id select new { New = item, Old = oldItem }; dbContest.SupplyItems.AddRange(itemsToInsert); foreach (var item in itemsToUpdate) { item.Old.SerialNumber = item.New.SerialNumber; … } dbContext.SaveChanges(); } } Поздняя вставка В комментариях написали, что эта схема будет работать только если вставка выполняется с одного клиента, а несколько клиентов будут друг другу мешать. Это верно. К сожалению, эта задача плохо решается даже не на уровне Entity Framework, а на уровне непосредственно БД. Даже если писать на чистом SQL, вставка/изменение большого количества записей с разных клиентов будет выполняться строго последовательно (как пишет Фаулер, в этом случае применяется паттерн Пессимистическая блокировка). Такие задачи решают не техническими, а организационными средствами. В моей практике был такой опыт, и там вставкой/обновлением занимался единственный клиент, фоновый процесс, который запускался несколько раз в день. Все остальные клиенты (рабочие места операторов) большую часть данных только читали, поэтому и проблем с блокировками было гораздо меньше. Конец вставки Можно также в явном виде вызывать SQL-запрос, который на сленге программистов называется UPSERT. Если запись уже есть, то это будет UPDATE, а если её нет, то INSERT. Для SQL Server запрос UPSERT делается с помощью команды MERGE. EF напрямую не поддерживает инструкцию MERGE, но он позволяет выполнять SQL-запросы. В новом EF Core это может быть сделано так: public void Add(IEnumerable items) { using (var dbContext = new MyDbContext()) { foreach (var item in items) { dbContext.Database .ExecuteSqlCommand(@"MERGE dbo.SupplyItems AS Target USING (SELECT @Id, @SerialNumber) AS Source (Id, SerialNumber) ON Source.Id = Target.Id WHEN MATCHED THEN UPDATE SET SerialNumber = Source.SerialNumber WHEN NON MATCHED THEN INSERT (Id, SerialNumber) VALUES (Source.Id, Source.SerialNumber)", new SqlParameter("@Id", item.Id), new SqlParameter("@SerialNumber", item.SerialNumber); } } } Недостатком такого подхода является то, что перечислять все параметры придётся вручную. Мы не можем воспользоваться средствами EF для генерации запроса. При изменении схемы мы можем просто забыть внести изменения и здесь тоже. Вторым недостатком является не очень высокая скорость внесения изменений. Чтобы ускорить, можно сделать групповую вставку во временную таблицу и затем вставлять/обновлять данные уже из неё. Плюсом будет то, что такой метод исключает конфликт идентификаторов, даже если вставка происходит с разных клиентов, и при этом не сильно блокирует систему. Ответ 2
public class DefaultContext: DbContext { public DefaultContext(DbConnection connection) :base(connection, true) { } public DbSetWorkers { get; set; } } public class Worker { public int Id { get; set; } [Index(IsUnique = true)] public string Name { get; set; } } Я сымитирую похожую ошибку, нарушение индекса. DbConnection connection = Effort.DbConnectionFactory.CreateTransient(); using (DefaultContext _context = new DefaultContext(connection)) { var worker = new Worker { Name = "Ivan" }; _context.Entry(worker).State = EntityState.Added; _context.SaveChanges(); _context.Entry(worker).State = EntityState.Detached; try { Console.WriteLine("from try"); _context.Entry(worker).State = EntityState.Added; _context.SaveChanges(); } catch (DbUpdateException dbEx) { var errors = dbEx.Entries.Select(x=>x.Entity).ToList(); foreach(var entity in errored) { //Убираем из контекста ошибочные сущности _context.Entry(entity).State = EntityState.Detached; } //Сохраняем _context.SaveChanges(); } } В данном примере я добавляю в бд одну запись, после этого пытаюсь добавить запись которая будет нарушать index. В блоке catch получаем строки которые вызвали ошибку при сохранении (dbEx.Entries.Select(x=>x.Entity)), убираем данные строки из контекста (_context.Enty(entity).State = EntityState.Detached) после этого сохраняем/оповещает пользователя Ответ 3
Прикладываю скорректированный код: public static int Insert(TEntity entity) where TEntity : class { using (var _db = new LicMngrEntities()) { _db.Database.Log = (s => System.Diagnostics.Debug.WriteLine(s)); try { _db.Entry(entity).State = EntityState.Added; _db.SaveChanges(); return 0; } catch (Exception ex) { MessageBox.Show("Ошибка при добавлении записи в БД" + ex.Message); return -1; } } } И код отправки на сохранение: PassportSystem_SupplyItem passSystemSuppItem = new PassportSystem_SupplyItem { id_SupItem_PassSys_SupItem = Convert.ToInt32(cbSelectSuppItem.SelectedValue), idSystem_PassSys_SupItem = IdSystem, serialNumberPassSys_SupItem = tbSerialNumb.Text, countPassSys_SupItem = Convert.ToInt32(tbCount.Text), dateEndPassSys_SupItem = Convert.ToDateTime(dpDateEnd.Text).Date, dateStartPassSys_SupItem = Convert.ToDateTime(dpDateStart.Text).Date, dateSupplyPassSys_SupItem = Convert.ToDateTime(dpDateSupply.Text).Date, notePassSys_SupItem = tbNote.Text }; if (Class_EntityConnection.FindPassSuppItem(passSystemSuppItem.idSystem_PassSys_SupItem, passSystemSuppItem.id_SupItem_PassSys_SupItem, passSystemSuppItem.serialNumberPassSys_SupItem) != null) { var result = MessageBox.Show("Данная запись уже существует в таблице, хотите ее обновить?", "Обновление записи в БД", MessageBoxButton.YesNo, MessageBoxImage.Information); if (result == MessageBoxResult.Yes) Class_EntityConnection.Update(passSystemSuppItem); } else Class_EntityConnection.Insert(passSystemSuppItem); Код поиска по БД: public static View_PassportSystems_SupplyItem FindPassSuppItem(int idSystem, int idSuppItem, string serialNumb) { using (var _db = new LicMngrEntities()) { return _db.View_PassportSystems_SupplyItem.Find(idSuppItem, idSystem, serialNumb); } } P.S. Ответы на вопросы по применению контекста будут добавлены после разбора темы у Тролсена Насчет автогенерируемого ключа в EF, его public static int SaveCustomerToDB(Customer _newCustomer) { try { Db.Customer.Add(_newCustomer); //добавляем объект к контексту Db.SaveChanges(); return _newCustomer.idCustomer; //возвращает автогенерируемы ключ } catch (Exception ex) { MessageBox.Show("Ошибка добавления нового заказчика в базу данных!" + ex.Message); return -1; } }
Комментариев нет:
Отправить комментарий