Az Entity Framework használata rengeteg elÅ‘nnyel jár, de az offline, cachelt működésébÅ‘l adódóan van egy hátránya, mégpedig az, hogy a konkurencia kezelését is az Entity Framework segÃtségével kell megoldanunk.
De mit is jelent a konkurencia adatbázisok esetén? Konkurenciával akkor kell foglalkoznunk, ha ugyanazt a rekordot többen is Ãrni szeretnék. Képzeljük el a szituációt. User 1 csatlakozik és kikéri a könyveket. Ugyanezt a műveletet User 2 is megteszi. MindkettÅ‘jüknél az Entity Framework cachelt adatok jelennek meg. User 2 az egyik könyv elérhetÅ‘ darabszámát megváltoztatja, majd idÅ‘vel User 1 is ugyanezt megteszi. A cache miatt User 1 adatai az Ãrás pillanatában már nem helyesek és Ãgy ezáltal felülÃrná User 2 módosÃtásait.
A probléma kikerülésére több megoldás is létezik:
- Elfelejtjük a cache-t
- Tárolt eljáráson keresztül frissÃtünk
- Entity Framework konkurencia kezelését alkalmazzuk
Az elsÅ‘ megoldás valójában nem megoldás, mivel ezzel elveszÃtjük az Entity Framework hatalmas elÅ‘nyét. A második megoldás abban az esetben jó, ha az adatbázis rendszerünk támogat tárolt eljárásokat, de ez sem tökéletes. Mégpedig azért nem, mert ha adatbázis szolgáltatást kell váltanunk, akkor ezeket a tárolt eljárásokat kézzel, SQL-ben kell megÃrnunk az új rendszerre.
Az igazi megoldás az EF konkurencia kezelése. Az EF konkurencia kezelése optimista konkurenciát feltételez, vagyis azt, hogy az adatütközések viszonylag ritkák. Ellentétben a pesszimista megközelÃtésekkel (amelyek elÅ‘re zárják az adatokat, és csak azután folytatják a módosÃtást) az optimista párhuzamosság nem vesz fel zárolást, hanem gondoskodik arról, hogy az adatmódosÃtás sikertelen legyen a mentéskor, ha az adatok megváltoztak a lekérdezés óta. ErrÅ‘l a hibáról a rendszer értesÃti az alkalmazást, amely ennek megfelelÅ‘en kezeli, esetleg úgy, hogy újra megpróbálja a teljes műveletet az új adatokon.
A konkurenciakezelés implementálásának elsÅ‘ lépése, hogy egy új tulajdonsággal látjuk el az entitás modellünket, amit megjelölünk konkurencia tokennek. Ennek a tulajdonságát az EF ugyanúgy nyomon követi, mint a többi tulajdonságét. A SaveChanges() hÃvás esetén ha Update vagy Delete művelet generálódik, akkor a konkurencia token tulajdonság értéke lekérdezésre kerül a művelet elvégzése elÅ‘tt és csak akkor lesz sikeres a művelet, ha a DB-bÅ‘l lekért érték megegyezik a Cache-ben tárolttal. EllenkezÅ‘ esetben egy DbUpdateConcurrencyException keletkezik, ami azt jelzi, hogy az adatot már módosÃtották.
Token konfigurálása
A konkurencia token konfigurálása függ az SQL kiszolgáló tÃpusától. A Microsoft SQL Server rendelkezik egy ROWVERSION tÃpussal, ami ezt teszi lehetÅ‘vé. Ennek a konfigurálásához a [Timestamp] attribútumot alkalmazhatjuk egy byte[] tulajdonságon.
Mivel ez a funkció nem minden adatbázis rendszeren támogatott, az EF biztosÃt egy általános megoldást is a [ConcurrencyCheck] attribútummal, amit egy Guid tÃpusú tulajdonságon alkalmazhatunk. Az alábbi példa a konkurencia kezeléssel kiegészÃtett Book modellt mutatja be:
internal class Book
{
public int ISBN { get; set; }
public string Title { get; set; } = null!;
public int PublishYear { get; set; }
public Author? Author { get; set; }
public Publisher? Publisher { get; set; }
public int AuthorId { get; set; }
public int PublisherId { get; set; }
[ConcurrencyCheck]
public Guid Version { get; set; }
}
A konkurencia token konfigurálása builder szintaxisban is lehetséges, a IsConcurrencyToken() hÃvás segÃtségével.
A DbUpdateConcurrencyException kezelése
A kivétel lekezelése nagymértékben alkalmazás és adat függő. Az egyik legegyszerűbb módszer, hogy tájékoztatjuk a felhasználót, hogy a mentés meghiúsult egymásnak ellentmondó változások miatt. Ezt követően a felhasználó betöltheti az új adatokat és újra próbálkozhat.
Egy másik lehetÅ‘ség, ha az alkalmazás automatizált frissÃtést hajt végre, akkor az adatok újbóli lekérdezése után egyszerűen újra próbálkozhat a mentéssel.
Egy sokkal kifinomultabb módja az ilyen hibák kezelésének a függőben lévő változások összevonása az adatbázisban lévő új értékekkel. Az összevonás pontos módja szintén alkalmazásfüggő. Az alábbi példa egy lehetséges kezelést mutat be:
using KonyvekContext context = new KonyvekContext();
//könyv kikérése
Book b = context.Books.Single(b => b.ISBN == 1);
//módosÃtás a cache-ben
b.ISBN = 1234;
bool saved = false;
while (!saved)
{
try
{
//mentés indÃtása
context.SaveChanges();
saved = true; //kilépés a ciklusból
}
catch (DbUpdateConcurrencyException ex)
{
foreach (EntityEntry entry in ex.Entries)
{
//ellenÅ‘rizzük, hogy a konkurencia hiba tényleg book tÃpusra történt-e
if (entry.Entity is not Book)
throw new NotSupportedException("This method only knows Book concurecy conflicts");
//Menteni kÃvánt értékek
PropertyValues current = entry.CurrentValues;
//db-ben tárolt értékek
var db = entry.GetDatabaseValues();
//itt lehet elvégezni az egyesÃtést.
//Ha nem törlés volt, akkor a DB-ben tárolt most a helyes
if (db != null)
{
current["ISBN"] = db["ISBN"];
//tárolt értékek befrissÃtése a konkurencia miatt
entry.OriginalValues.SetValues(db);
}
}
}
}