Паралелност на база данни се отнася до ситуации, при които множество процеси или потребители имат достъп или променят едни и същи данни в база данни едновременно. Контролът на паралелността се отнася до специфични механизми, използвани за осигуряване на съгласуваност на данните при наличие на едновременни промени.
EF Core внедрява оптимистичен контрол на паралелността, което означава, че ще позволи на множество процеси или потребители да правят промени независимо без допълнителни разходи за синхронизиране или заключване. В идеалната ситуация тези промени няма да си пречат една на друга и следователно ще могат да успеят. В най-лошия случай два или повече процеса ще се опитат да направят противоречиви промени и само един от тях трябва да успее.
Как работи контролът на паралелността в EF Core
Свойствата, конфигурирани като токени за паралелност, се използват за прилагане на оптимистичен контрол на паралелността: всеки път, когато се извършва операция за актуализиране или изтриване по време на SaveChanges
, стойността на токена за паралелност в базата данни се сравнява с оригиналната стойност, прочетена от EF Core.
- Ако стойностите съвпадат, операцията може да завърши.
- Ако стойностите не съвпадат, EF Core приема, че друг потребител е извършил конфликтна операция и прекъсва текущата транзакция.
Ситуацията, когато друг потребител е извършил операция, която е в конфликт с текущата операция, е известна като конфликт на едновременност.
Доставчиците на бази данни са отговорни за прилагането на сравнението на стойностите на токените за паралелност.
В релационни бази данни EF Core включва проверка за стойността на токена за паралелност в клаузата WHERE
на всеки израз UPDATE
или DELETE
. След изпълнение на изразите, EF Core чете броя на редовете, които са били засегнати.
Ако няма засегнати редове, се открива конфликт на едновременност и EF Core хвърля DbUpdateConcurrencyException
.
Например, може да искаме да конфигурираме LastName
на Person
да бъде токен за едновременност. След това всяка операция за актуализиране на Person ще включва проверката за едновременност в клаузата WHERE
:
SQLCopy
UPDATE [Person] SET [FirstName] = @p1
WHERE [PersonId] = @p0 AND [LastName] = @p2;
Разрешаване на конфликти на едновременност
Продължавайки с предишния пример, ако един потребител се опита да запази някои промени в Person
, но друг потребител вече е променил LastName
, тогава ще бъде хвърлено изключение.
В този момент приложението може просто да информира потребителя, че актуализацията не е успешна поради противоречиви промени и да продължи. Но може да е желателно да подканите потребителя да се увери, че този запис все още представлява същото действително лице и да опитате отново операцията.
Този процес е пример за разрешаване на конфликт на едновременност.
Разрешаването на конфликт на паралелност включва обединяване на чакащите промени от текущия DbContext
със стойностите в базата данни. Кои стойности се обединяват ще варират в зависимост от приложението и могат да бъдат насочени от потребителски вход.
Има три набора от стойности, които помагат за разрешаване на конфликт на паралелност:
- Текущи стойности са стойностите, които приложението се е опитвало да запише в базата данни.
- Оригиналните стойности са стойностите, които първоначално са били извлечени от базата данни, преди да бъдат направени каквито и да било редакции.
- Стойностите на базата данни са стойностите, съхранявани в момента в базата данни.
Общият подход за справяне с конфликт на едновременност е:
- Хванете
DbUpdateConcurrencyException
по време наSaveChanges
. - Използвайте
DbUpdateConcurrencyException.Entries
, за да подготвите нов набор от промени за засегнатите обекти. - Обновете оригиналните стойности на токена за едновременност, за да отразите текущите стойности в базата данни.
- Повторете процеса, докато не възникнат конфликти.
В следващия пример Person.FirstName
и Person.LastName
са настроени като токени за едновременност. Има // TODO:
коментар в местоположението, където включвате специфична за приложението логика, за да изберете стойността, която да бъде запазена.
using (var context = new PersonContext()) { // Fetch a person from database and change phone number var person = context.People.Single(p => p.PersonId == 1); person.PhoneNumber = "555-555-5555"; // Change the person's name in the database to simulate a concurrency conflict context.Database.ExecuteSqlCommand( "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");
var saved = false; while (!saved) { try { // Attempt to save changes to the database context.SaveChanges(); saved = true; } catch (DbUpdateConcurrencyException ex) { foreach (var entry in ex.Entries) { if (entry.Entity is Person) { var proposedValues = entry.CurrentValues; var databaseValues = entry.GetDatabaseValues();
foreach (var property in proposedValues.Properties) { var proposedValue = proposedValues[property]; var databaseValue = databaseValues[property];
// TODO: decide which value should be written to database // proposedValues[property] = <value to be saved>; }
// Refresh original values to bypass next concurrency check entry.OriginalValues.SetValues(databaseValues); } else { throw new NotSupportedException("Don't know how to handleconcurrency conflicts for " + entry.Metadata.Name); } } } } }