İlkay İlknur

just a developer...

Entity Framework Second Level Cache

Bu makaleye Github üzerinden katkıda bulunabilirsiniz.

Katkıda Bulunanlar : Daron Yöndem

Geliştirdiğimiz uygulamalar büyüdükçe ve karmaşıklaştıkca uygulamaların kullandıkları databaselere de erişimler artmakta. Uygulamanın aldığı request sayısı arttıkça bir noktadan sonra her işlem için database'e gitmek uygulamanın performansını gözlü görülür bir biçimde etkilemekte. Özellikle uygulamanız içerisinde readonly tablelarınız varsa bu tablelara attığınız sorguların yanıtlarını cacheleyip bir sonraki requestte database'e gitmeden cachelenmiş datadan işlem yapmak uygulamanızın performansını arttıracaktır.

Entity Framework tarafındaki cache yapısına baktığımızda aslında DbContext içerisinde bir caching yapısı olduğunu görüyoruz. First level cache dediğimiz bu cache elinizde bulunan DbContext ile yaşıyor ve DbContext'i dispose ettiğinizde de elinizden gidiyor. Aslında bu bile bir noktaya kadar size performans açısından fayda sağlar. Ancak yukarıda bahsettiğimiz gibi database'den dönen sorgu sonucunu kendisinden sonra gelecek olan sorgularda da kullanabiliyor olmak daha da kritik bir öneme sahip. İşte bu noktada second-level cache devreye giriyor.

Entity Framework içerisine baktığımızda aslında default olarak gelen veya tek bir config değeriyle aç&kapa yapılabilecek bir second-level cache implementasyonu olmadığını görüyoruz. Aslında Entity Framework 6 ile gelen Interception yapısıyla second-level caching yapabilmek çok da zor değil. Neyse ki Microsoft'ta EF ekibinden bir kişi bu işe girişmiş ve second-level cache implementasyonunu yapmış. Proje sayfasına codeplex üzerinden ulaşabilirsiniz.

Şimdi gelelim bu implementasyonu uygulamalarımızda nasıl kullanacağımıza. Öncelikli olarak Nuget üzerinden libraryi projemize ekliyoruz.

PM> Install-Package EntityFramework.Cache

Library'i projeye ekledikten sonra gelelim second-level cache'i enable etme kısmına. Eğer entity framework configuration class'ınız varsa doğrudan bu class içerisinden ilerleyebilirsiniz. Eğer böyle bir classınız yoksa DbConfiguration classından türeyen bir configuration classı yazmak gerekiyor.

public class MyDbConfiguration : DbConfiguration
{
    public MyDbConfiguration()
    {
        var transactionHandler = new CacheTransactionHandler(new InMemoryCache());
        AddInterceptor(transactionHandler);
        var cachingPolicy = new CachingPolicy();
        Loaded +=
            (sender, args) => args.ReplaceService<DbProviderServices>(
            (s, _) => new CachingProviderServices(s, transactionHandler,
                cachingPolicy));
    }
}

Classı yazdıktan sonra yukarıda gördüğünüz gibi database operasyonları arasına girecek olan bir interceptor register ediyoruz. Bu register ettiğimiz CacheTransactionHandler tipi de zaten EF second-level cache librarysi içerisinde bulunuyor. Sonrasında da CachingPolicy vererek hızlı bir şekilde implementasyonu şimdilik bitiriyoruz. Tüm bunları yaptıktan sonra default değerlerle EF second-level cache hizmetinizde. Default değerlere göre database'den dönen değerler memoryde sonsuza kadar cachelenecek.

Biraz daha derinlere inip baktığımızda aslında entity framework'ün second-level cache kütüphanesi içerisinde bir dictionary tutuğunu ve bu dictionary içerisinde de key-value şeklinde db'den dönen yanıtları sakladığını görüyoruz. Peki key olarak hangi değer kullanılıyor diye sorduğumuzda ise içerisinde şu şekilde bir implementasyon olduğunu görüyoruz.

Key={DBName}-{SQLQuery}

Yukarıdaki key yapısına baktığımızda 2 farklı sonuç çıkarabiliriz. Bunlardan biri eğer uygulamanız multi-tenant bir uygulama ise yani aynı DbContext nesnesiyle farklı databaseleri sorguluyorsanız second-level cache bunu başarılı bir şekilde yönetebiliyor. Bir diğer konu da SQL Query bazında cacheleme yapması. Yani EF ile yaptığınız iki farklı sorgu sonucunda ikisinde de aynı entityler dönüyorsa bu entityler 2 kere cachelenecek. Bunu da göz önünde bulundurmakta fayda var.

Entity Framework second-level cache ile ilgili temel konulara değindik. Şimdi geldi sıradaki özelleştirme bölümüne.

Farklı Cache Store Providerlar

Entity Framework second level cache kütüphanesi ile farklı cache store providerları kullanmak mümkün. Ancak kütüphane içerisinde şu an sadece InMemory cache store providerı var. İnternet'te araştırma yaptığımda Redis providerı bulabildim. Ancak custom providerlar da yazmak mümkün. Sonuçta herşey ICache'i implemente etmeye bakıyor.

Custom Caching Policy

Entity Framework second-level cache kütüphanesi default olarak herşeyi cacheliyor. Bu da duruma göre doğru bir şey olmayabilir. Eğer uygulamanızda sorgu çektiğiniz tablolara update ve insert yapılıyorsa sorgular sonucunda yapılan update ve insertleri okuma işleminde göremezsiniz. Bu nedenle özel bir CachingPolicy tanımlayıp işimize yarayan yerleri cachelemekte fayda var. Gelelim bu işi nasıl yapacağımıza.

Kendi CachingPolicy'mizi yazmak için yeni bir class yazıp o classı da CachingPolicy sınıfından türetmek gerekiyor. Sonrasında da CachingPolicy içerisindeki üç farklı metodu override ederek kendi caching policymizi yaratabiliyoruz. Caching policy içerisinde override edilebilecek olan üç metot şu şekilde.

public class MyCachingPolicy : CachingPolicy
{
    protected override void GetExpirationTimeout(System.Collections.ObjectModel.ReadOnlyCollection<System.Data.Entity.Core.Metadata.Edm.EntitySetBase> affectedEntitySets, out TimeSpan slidingExpiration, out DateTimeOffset absoluteExpiration)
    {
        absoluteExpiration = DateTimeOffset.MaxValue;
        slidingExpiration = TimeSpan.MaxValue;
    }

    protected override bool CanBeCached(System.Collections.ObjectModel.ReadOnlyCollection<System.Data.Entity.Core.Metadata.Edm.EntitySetBase> affectedEntitySets, string sql, IEnumerable<KeyValuePair<stringobject>> parameters)
    {
        return base.CanBeCached(affectedEntitySets, sql, parameters);
    }

    protected override void GetCacheableRows(System.Collections.ObjectModel.ReadOnlyCollection<System.Data.Entity.Core.Metadata.Edm.EntitySetBase> affectedEntitySets, out int minCacheableRows, out int maxCacheableRows)
    {
        base.GetCacheableRows(affectedEntitySets, out minCacheableRows, out maxCacheableRows);
    }
}
  • GetExpirationTimeout metodunda adından da anlayabileceğiniz üzere cache'in expire olma süresini verebiliyoruz. Absolute ve sliding expiration vermek mümkün.
  • CanBeCached metodunda ise sorgu sonucunda dönen entitylerin second-level cache tarafından cachelenip cachelenmeyeceğini söyleyebiliyoruz.
  • GetCacheableRows metodunda ise cachelenecek minimum ve maximum row sayısını söyleyebiliyoruz.

Bu özelleştirme yapısında karşımıza çıkabilecek en önemli sorun cache policy configlerini nerede tutacağımız. Yani varsayalım ki contextiniz içerisinde on entityden sadece üçü için yapılacak sorguları cachelemek istiyorsunuz ve bu üç entity için de farklı expiration vermek istiyorsunuz. Bu configleri cache policy classınız içerisine koyabilirsiniz. Ama daha generic bir çözümle custom attribute yazıp oradan bu dataları çekmek isterseniz bunu yapmak biraz zor. Çünkü öncelikle size parametre olarak gelen affectedEntitySets içerisinden dönen datanın hangi table'dan döndüğünü almanız gerekiyor. Bu bilgiyi EntitySetBase içerisindeki Table propertysinden alabilirsiniz. Sonrasında da Entity Framework metadatasını sorgulayarak o table'a map olan class'ı(Detaylı bilgi için) almanız gerekecek. Oradan da reflectionla attribute'ü okuyup ona uygun olarak değerleri verebilirsiniz. Bu durum biraz zor ve performansı düşüren bir uygulama olacaktır. Açıkcası EF metadatasını sorgulamanın performansa nasıl bir etki getireceğini test etmedim. O yüzden bu noktadan ilerlenecekse öncelikle bazı testlerin yapılmasında fayda var.

Entity framework second-level cache kütüphanesi özellikle readonly tabloların cachelenmesi konusunda basitliğini de göz önünde bulundurduğumuzda güzel bir çözüm. Ancak işler kompleksleştiğinde kullanımı oldukça zorlaşıyor. Örneğin istediğimiz durumda cache'i invalide etmemiz mümkün değil. Bunun yanında transaction içerisinde yapılan sorguların da cachelenmediğinden bahsetmemde fayda var. Projenin içerisinde bu özellik ToDo olarak bulunuyor. Ancak ne zaman kütüphane içerisine eklenir sorusuna cevap vermek zor.



Yorum Gönder