Entity Framework Core – Data Concurrency

Merhaba,

Geliştirdiğiniz yazılım uygulaması, eş zamanlı olarak son kullanıcılar tarafından yoğun bir işlevsel trafiğe maruz kalıyor ve bu yüzden veritabanı üzerinde fazlasıyla CRUD işlemleri söz konusu oluyorsa ‘veri tutarlılığı‘ sizler için oldukça önem arz ediyordur. Uygulama, veritabanı aracılığıyla organize ettiği verileri en güncel ve en temiz haliyle son kullanıcıya sunabilmeli ve böylece ‘stale data’ yahut ‘dirty data’ şeklinde nitelendirilen işlevsiz verileri kullanıcıdan soyutlayabilmelidir. Bu girizgah direkt olarak aklınızda “hocam bayat veri nasıl oluyor?” sorunsalını doğurabilir… İşte bizler bu içeriğimizde eş zamanlı işlemlerde olası veri tutarsızlıklarına sebebiyet verebilecek olan durumları gün yüzüne çıkaracak ve bir yandan da bu tarz durumlarda nasıl refleks göstermemiz gerektiğine dair uzun uzun istişaremizi edecek ve konuya dair gerekli örneklendirmeler eşliğinde derinlemesine izahatimizi sunarak, aklınıza gelebilecek bu suali sindire sindire cevaplandıracağız. Hadi gelin başlayalım…

İlk olarak gelin herşeyden önce olası veri tutarsızlıklarına sebep olabilecek bir örnek olayı ele alarak başlayalım ve stale/dirty data tabirinde olduğu gibi bayat verinin ne olduğunu burada görmeye çalışalım;

Bir ÖSYM sınavına “ais.osym.gov.tr” sitesi üzerinden başvuruda bulunduğunuzu varsayalım. Bir müddet sonra sınav bölgesine dair “Çankaya” olarak seçtiğiniz bilgiyi “Altındağ” olarak değiştirme kararı verdiğinizi düşünelim. Ve bu işlem için gittiniz bilgisayarınızdan ilgili site üzerinden gerekli sayfayı açtınız. Tam gerekli işlemleri yapacaksınız, tak! babanız telefondan arıyor. “Efendim babacığım…” şeklinde muhabbete girerek babanız tarafından en son yaptığınız vukatlara istinaden size olan sövgülerini dinlerken bir yanda gayri ihtiyari bilgisayarınızdan uzaklaştınız. Babanızın sövgüsü uzun sürüyor, sizin aklınızda sınav bölgesine dair bilgiyi değiştirmek var. Babanız konuşurken biryandan da telefondan ilgili siteyi açarak tekrar değişiklik yapacağınız sayfayı açtığınızı düşünün. Bu an itibariyle size ait bilgiler aynı sistem üzerinde iki farklı cihaz tarafından açılmış ve her ikisinde de “Çankaya” şeklinde gözükmekte. Şimdi telefondan “Çankaya” bilgisini “Altındağ” olarak değiştiriyor ve ardından güncelle butonuna basıyorsunuz. Bu işlemden sonra ilgili sitenin veritabanında size ait olan bilgi “Çankaya” iken “Altındağ” olarak güncellenmiştir. Babanızın sövgüsü sona erdikten sonra bilgisayarınıza gidiyorsunuz anah bir bakıyorsunuz ilgili ösym sitesi açık ve hala bilginiz “Çankaya” şeklinde gözükmektedir…

Eee! hani biz bu “Çankaya” bilgisini “Altındağ” olarak değiştirmemiş miydik? Evet, değiştirmiştir. Ama bu işlemi mobil üzerinden gerçekleştirmiştik. Dolayısıyla bilgisayarda açık olan bu sayfa bu güncelleme işleminden önceki veriler tarafından şekillendirilmiş olduğundan ve dolayısıyla sayfa tekrar güncellenmediğinden dolayı buradaki “Çankaya” bilgisi esasında artık bir stale/dirt data dediğimiz bayat veridir. Çünkü o veri artık “Çankaya” değil, “Altındağ”dır.

Neyse, şimdi ne olur ne olmaz diyerek işi garantiye almak için bilgisayardaki ekrandan da “Çankaya” bilgisini “Altındağ” ile değiştirerek tekrardan güncelle butonuna tam basacakken “dur ulan! Keçiören bana daha uygun sanki” diyerekten “Keçiören” bilgisini seçip güncelle butonuna basıyorsunuz.

İlk etapta mobilden “Çankaya” olan veriyi “Altındağ” yaptığımızı biliyoruz ama ikinci etapta bilgisayar üzerinden yapılan işin “Çankaya” dan “Keçiören” şeklinde olduğunu zannediyoruz. Zannediyoruz çünkü gerçekte olan “Altındağ” bilgisini “Keçiören” olarak güncellemekteyiz.

Buradaki her iki durumuda aşağıdaki gibi şematik olarak resmedebiliriz;

1. Güncelleme 2. Güncelleme
Entity Framework Core - Data Concurrency Entity Framework Core - Data Concurrency

Dolayısıyla nihai olarak ilgili sitede size ait olan verilerde sınav bölgesi olarak “Keçiören” bilgisi tutulmaktadır. Ama bu neticeye varana kadar yukarıdaki örnek olayda ele aldığımız olası durum ceyran ettiğinden dolayı “Keçiören” bilgisine gelene kadar aslında süreçte birçok veri söz konusu olmuş oldu… Nihayetinde böyle bir durumu en iyi Last In Wins şeklinde tarif etmek oldukça yakışık olacaktır…(son gelen kazanır) Düşünsenize! Size ait log dosyasında zahiren yaptığınız değişikliklerden farklı kayıtlar tutulmakta ve iddianızın aksine bunlar farkında olmasanızda esasında gerçekten oynadığınız veriler olmaktadır… Bir kullanıcının düşebileceği ve izahını zor yapabileceği hazinliklerden biriside bu olsa gerek 🙂

İşte bu ve buna benzer stale/dirt data dediğimiz bayat verilerin söz konusu olduğu durumlarda veri tutarsızlığından söz edebilmekteyiz. Eş zamanlı çalışmalarda oluşabilecek veri tutarsızlıklarına karşılık doğru stratejileri benimsemeli ve gerekli önlemleri almalıyız. Bunun için kullanılan ORM çeşidine göre farklı çözümler sunulmakla beraber bizler bu içeriğimizde başlıkta da ifade edildiği gibi Entity Framework Core üzerinde gerekli önlemleri ele alacağız. Bu önlemlerimiz Data Concurrency dediğimiz veri tutarlılığına dair önlemler olacaktır. Data Concurrency yani Veri Tutarlılığı sağlayabilmek için genellikle üzerinde o an işlem yapılan veriye bir kilitleme(lock) operasyonu uygulanarak yapılan değişikliğin hangi veri üzerinde gerçekleştiğine dair bilgi tutulabilmekte ve bu şekilde CRUD işlemlerinde hangi veri üzerinde çalışıldığına dair kesin ve net olunabilmektedir.

Entity Framework Core - Data ConcurrencyBu kilitleme(locking) işlemi için iki farklı yaklaşım söz konusudur. Bu yaklaşımlar;

  • Pessimistic Lock (Kötümser Kilitleme)
  • Optimistic Lock (İyimser Kilitmele)

şeklinde ifade edilmektedir. Şimdi gelin bu her iki yaklaşımıda hem pratik hem teorik olmak üzere detaylıca ele alalım.

Pessimistic Lock (Kötümser Kilitleme)

Uygulama üzerinde veritabanına dair CRUD işlemleri gerçekleşirken veri tutarlılığını korumak ve bayat verilere mahal vermemek için ilgili data kilitlenerek o anki işlem her ne ise eş zamanlı olarak herhangi bir güncelleme yahut değişiklik yapılması engellenmiş olur. Bu işlem transaction aracılığıyla session bazlı gerçekleştirilir ve o anda yapılan operasyonel işleme özel açılan session nihai olarak işlevselliğini bitirinceye yahut rollback ile tüm işlemlerin geri alınmasına dair talimat alınıncaya dek ilgili satır(row) veritabanında kilitlenir(locking). Bu yaklaşım “Deadlock” diye nitelendirdiğimiz “Kilitleme Çıkmazı” ya da “Ölüm Kilitlenmesi” dediğimiz kilitlenmelere sebep olduğunuda unutmamak gerekir.

Entity Framework Core, Pessimistic Lock yaklaşımına dair herhangi bir desteği bünyesinde barındırmamaktadır. Bu durumda EF Core ile Pessimistic Lock yaklaşımını uygulamak istiyorsanız, ilgili ORM tarafından generate edilecek olan SQL komutlarına bir şekilde WITH (XLOCK) ifadesini dahil etmemiz gerekmektedir ve böylece veritabanındaki hem satır hem de tablo komple locking edilmiş olacaktır.(Bu XLOCK ifadesinin özelliğidir. Konuya dair detaylı açıklamayı şuradaki adresten edinebilirsiniz.)

Aşağıdaki örnek kod bloğunu inceleyebilirsiniz;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EFDataConcurrency.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;

namespace EFDataConcurrency.Controllers
{
    public class HomeController : Controller
    {
        NorthwindContext _northwindContext;
        public HomeController(NorthwindContext northwindContext) => _northwindContext = northwindContext;
        public async Task<IActionResult> Index()
        {
            using (IDbContextTransaction transaction = await _northwindContext.Database.BeginTransactionAsync())
            {
                string sql = "SELECT * FROM Personeller WITH (XLOCK)";
                List<Personeller> personellers = _northwindContext.Personeller.FromSqlRaw(sql).ToList();
                return View(personellers);
            }
        }
    }
}

Optimistic Lock (İyimser Kilitmele)

Pessimistic Lock’a nazaran herhangi bir locking işlemi olmaksızın update edilecek olan verinin bayat olup olmadığını anlamamızı sağlayan yaklaşımdır. Bunun için ilgili datanın ilgili tabloda yer alan versiyon numarası olarak nitelendirilen bir kolonda bulunan değeri match edilir ve neticede eşleşme doğrulanmazsa o anki operasyonda bayat verilerin olduğu anlaşılarak işlem geri alınır.

Entity Framework Core; her ne kadar Pessimistic Lock’a karşı bir özellik barındırmasada, Optimistic Lock yaklaşımına dair genetiğinde yapısal bir özellik taşımaktadır ve operasyonel olarak bizlere bir sorumluluk bırakmaksızın süreci idare etmektedir. Burada yukarıdaki satırlarda bahsedildiği gibi güncelleme işlemi için SELECT sorgusuyla elde edilen satırın kendisine ait bir versiyon numarasını memory’de saklayarak, update transaction’ı execute edildiğinde veritabanındaki orjinal haliyle karşılaştırmakta ve tüm bu süreci otomatik olarak kendiliğinden gerçekleştirmektedir. Eğer ki ilgili satırın versiyon numarası veritabanındakiyle benzerlik gösteriyorsa +1 arttırıp işlemlerin neticelenmesine ve tüm sürecin commit olmasına izin veriyor, yok eğer benzerlik göstermiyorsa kullanılan ORM’nin türüne göre hata fırlatıyor…

EF Core ile Optimistic Lock ile tutarsız verileri engellemek ve conflictleri(çakışma) ortadan kaldırabilmek için;

  • Property Based Configuration (ConcurrencyCheck Attribute)
  • RowVersion Column

şeklinde olmak üzere iki farklı yöntemi uygulayabiliriz.

Property Based Configuration (ConcurrencyCheck Attribute)

Bu yöntemde, bayat veriye mahal vermek istemediğiniz her bir property için ConcurrencyCheck attribute’u ile güvence sağlamakta ve ilgili propertye özel token oluşturarak conflictlere engel olmamızı sağlamaktadır.

    public class Ogrenci
    {
        public int Id { get; set; }
        [ConcurrencyCheck]
        public string Adi { get; set; }
        public string Soyadi { get; set; }
    }

Yukarıdaki örnek kod bloğunu ele alırsak, “Adi” propertysi ConcurrencyCheck attribute’u ile işaretlenerek Concurrency Token sayesinde olası bayat veriye karşı güvenceye alınmış bulunmaktadır.

Ayrıca bayat veriye karşı güvenceyi farklı olarak aşağıdaki yollada gerçekleştirebilir ve Concurrency Token sağlayabilirsiniz.

    public class OkulDB : DbContext
    {
        public OkulDB(DbContextOptions<OkulDB> dbContext) : base(dbContext) { }

        virtual public DbSet<Ders> Dersler { get; set; }
        virtual public DbSet<Ogrenci> Ogrenciler { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Ogrenci>().Property(_ => _.Adi).IsConcurrencyToken();
        }
    }

IsConcurrencyToken fonksiyonu ile ilgili propertye Concurrency Token set edilmektedir.

Yukarıda Concurrency Token için ele aldığımız her iki yöntemde, üzerinde çalıştığımız verinin modelini retrieve ederken bir concurrency token değeri oluşturacak ve oluşturulan bu concurrency token değerini generate edilecek olan Update yahut Delete sorgularına where koşulu olarak ekleyecek ve operasyonel durumda bu token değeri kontrol edilerek işlem gerçekleştirilecektir. Bu token değerinin üretildiği kolonlardan herhangi biri update yahut delete operasyonları esnasında değiştirildiği taktirde aşağıdaki ekran görüntüsünde olduğu gibi “DbUpdateConcurrencyException” tipinden bir istisna throw edilecektir.

Entity Framework Core - Data Concurrency

Eş zamanlı gerçekleştirilen bir senaryoyu canlandırmak için bu örnekte farklı contextler üzerinde işlem ele alınmaktadır. Dikkat ederseniz bir context tarafından işleme alınan model, bir başka context tarafından işleme tabi tutulamamaktadır. Dolayısıyla olası veri tutarsızlığının önü engellenmiş, bayat veriler etkisizleştirilmiştir.

Hatanın metinsel halini aşağıya alalım;

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: ‘Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.’

RowVersion Column

Uygulamalarımızda veri tutarlılığını sağlayabilmek için uygulayabileceğimiz ikinci yöntem ise RowVersion bilgisi tutmaktır. Üzerinde Data Concurrency sağlayacağımız verilerin bulunduğu ilgili tabloya, manuel oluşturacağımız concurrency token ya da versiyon bilgisini tutacak bir kolon oluşturarak concurrency conflictlere engel olabiliriz. Bu kolon genellikle “RowVersion” olarak isimlendirilmektedir. İlgili satıra özel tutacağı verinin sayısal olması ve bunun ardışık olarak kendiliğinden incremental(artan) olması oldukça makul olacaktır. Bu sebepten ötürü ilgili kolonun sayısal olması gerekmektedir. Tutulan bu RowVersion bilgisini süreçte ilgili satıra dair operasyonel olarak yapılan herbir işlemde birer birer arttıracağız ve değişiklikleri veritabanına fiziksel işlemeden önce son kez versiyon bilgisini check edeceğiz. Eğer ki verinin versiyonu, yapılan operasyon talebi esnasında değişmişse eş zamanlı bir değişikliğin söz konusu olmasından dolayı “DbUpdateConcurrencyException” hatası tekrar throw edilecektir. Böylece ilgili satır üzerinde eş zamanlı olarak yapılan tüm işlemlerde olası veri tutarsızlığına karşı önlem almış olacağız.

RowVersion yöntemini bir tabloya uygulayabilmek için yapılması gereken aşağıdaki gibi ilgili tablonun entity classına bir “RowVersion” isminde property eklemektir. Burada dikkat edilirse ilgili kolon bir byte dizisi olarak tanımlanmaktadır ve Timestamp data annotationı ile işaretlenmektedir.

    public class Ogrenci
    {
        public int Id { get; set; }
        public string Adi { get; set; }
        public string Soyadi { get; set; }
        [Timestamp]
        public byte[] RowVersion { get; set; }
    }

Burada Timestamp attribute’u sayesinde generate edilecek olan kolon Timestamp tipinden olacaktır ve Code First yaklaşımında bu kolon otomatik olarak concurency check için kullanılacaktır.
Entity Framework Core - Data Concurrency

Tabi her yöntemde olduğu gibi bu yöntemde de birden fazla alternatif mevcuttur. Mesela yukarıdaki işlemi Fluent Api kullanarak mapping işlemiylede aşağıdaki gibi gerçekleştirip, ilgili kolona RowVersion bilgisini set edebilirsiniz.

    public class Ogrenci
    {
        public int Id { get; set; }
        public string Adi { get; set; }
        public string Soyadi { get; set; }
        public byte[] RowVersion { get; set; }
    }
    public class OkulDB : DbContext
    {
        public OkulDB(DbContextOptions<OkulDB> dbContext) : base(dbContext) { }
        virtual public DbSet<Ders> Dersler { get; set; }
        virtual public DbSet<Ogrenci> Ogrenciler { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Ogrenci>().Property(_ => _.RowVersion).IsRowVersion();
        }
    }

Bu şekilde yapılan bir çalışma neticesinde yine eş zamanlı herhangi bir işlev söz konusu olduğunda RowVersion bilgisi değişmiş olacağından dolayı yine “DbUpdateConcurrencyException” hatası throw edilecek ve böylece data conflict engellenmiş olacaktır.

Netice olarak; veri tutarsızlığının söz konusu olduğu Data Concurrency durumlarında, bu tutarsızlığa mahal verebilecek durumun ya Optimistic ya da Pessimistic yaklaşımlarıyla bir şekilde farkına varılıp engellenmesi ve son kullanıcıya bu durumda olabilecek olan exceptionı hiç hissettirmeden doğru veriler üzerinde çalışabileceği bir şekilde yönlendirilebilmesi sağlanabilmektedir.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir