Kodun Altında Yatan Değerler: Entity Framework Derinlemesine İnceleme [1/2]

Entity, veri tabanında tablolara karşılığı olan sınıflara (class) denir.

Cihat Solak
Intertech

--

Entity Framework — Entity Framework Core — ORM

Entity Framework (EF) ve Language Integrated Query (LINQ) birer teknoloji olarak nitelendirilir. Object-Relational Mapping (ORM) ise veri erişim tekniğidir, bir teknoloji değildir! 💎

DBContext nesnesinin yaşam döngüsü transaction nedeniyle Transient olmamalıdır (Performansı da olumsuz etkiler). Çünkü, transaction yapısı bozulur. Ayrıca DbContext sınıfı thread safe olmadığı için DbContext nesnesini farklı thread’lerde kullanmak Task.Run(() => . )problem yaratabilir.

Entity State

Microsoft.EntityFrameworkCore namespace’i altında yer alan EntityState enum’ı hayati 🆘 önem taşımaktadır. EntityState, varlık sınıflarının mevcut durumunu bildirmektedir. Bu durum varlıklar (entities) üzerinde yapılacak herhangi bir işlemde değişebilir/değişecektir.

🧷 Detached (Tarafsız, Bağımsız): Entity, DbContext sınıfı tarafından izlenmemektedir. Dolayısıyla DbContext ile entity arasından herhangi bir bağlantı yoktur. Bir başka bakış açısıyla, entity sınıfı entity framework tarafında bellekte (memory) de olmadığı sürece durumu detached (tarafsız, bağımsız) olarak görüntülenecektir.

Detached — Change Tracker — EntityFramework Core

🔒 Unchanged (Değişmemiş): DbContext sınıfı tarafından sağlanan, EF Core tarafında belleğe (memory) alınmış fakat entity üzerinde herhangi bir değişiklik yapılmadığını ifade etmektedir.

Unchanged— Change Tracker — EntityFramework Core

🔎 Tracking — Context.Update İlişkisi

DbContext sınıfı tarafından sağlanan _context.Update() metotu sadece ve sadece izlenmeyen (track) entityler için kullanılmalıdır. İzlenen (tracking) entity için update metodunu kullanmak gereksizdir. Çünkü izlenen bir entity üzerinde işlem yapıldığında EF Core entity’e ait durumu ilgili şekilde güncellemektedir.

Update — Change Tracker — EntityFramework Core

Change Tracker — Don’t Repeat Yourself 📍

SORU 1: Veri tabanında 20–30 adet tablonuz olduğunu ve bu tabloların DbContext sınıfında da DbSet<T> lerinin olduğunu düşünelim. Her tabloda CreatedDate ve UpdatedDate sütunlarımız (columns) bulunmaktadır. Dolayısıyla her yaptığım Insert/Update işleminde ilgili değerleri rahat bir şekilde nasıl set ederim?

Gist’de yer aldığı gibi CreatedDate, UpdatedDate propertylerini içeren bir sınıf oluşturup, diğer sınıflardan bu sınıf miras alınabilir. Daha sonra DbContext sınıfının SaveChanges metotu override edilerek ChangeTracker yardımıyla ilgili propertyler set edildikten sonra veri tabanına yansıtılması sağlanır. Böylece kodun herhangi bir yerinde ilgili propertyleri tekrar set etmeme gerek kalmayacaktır. ✅ Çünkü SaveChanges yöntemi kullandığı anda override edilen metot devreye girecek ve ilgili tarihleri set edecektir.

SaveChanges — Change Tracker — EntityFramework Core

Eğer BaseEntity sınıfından faydalanmak istemiyorsanız, Shadow Property’leri de kullanabilirsiniz. Örneğin mevcut tüm tablolarınızda CreatedDate, UpdatedDate 🕖 sütunlarının varlığından eminseniz, aşağıdaki şekilde set edebilirsiniz.

Shadow Properties — Change Tracker — EntityFramework Core

DbContext Özellikleri

🔎 ChangeTracker: Bellekte (memory) track edilen verilere (entity) erişmemize imkan sağlar.

🔑 ContextId: Özellikle uygulamada birden fazla DbContext örneği (instance) ile çalışıyorsanız efektif bir propertydir. Örneğin loglama yapmak istediğinizde her bir instance’ı birbirinden ayırt etmek için kullanılabilir. Dolayısıyla Entity Framework Core her bir DbContext instance için ContextId atamaktadır. Loglarken bu instance’ları dahil edebilirsiniz.

ContextId— DbContext— EntityFramework Core

AsNoTracking(): Bu metot ile Entity Framework Core tarafından veri izlenmez (tracking). Dolayısıyla bellekte yer kaplamaz. Uygun koşullarda performanstan kazanç sağlar.

Track işlemini global olarak kapatmak isteyebilirsiniz. Bunu iki farklı şekilde belirleyebilirisiniz.

(1) DbContext sınıfının OnConfiguring metodunu override ederek

public class FinanceDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
}

(2) DI Container’a kayıt işlemi esnasında kolayca yapabilirsiniz.

builder.Services.AddDbContext<FinanceDbContext>(options =>
{
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

Hatırlatma: EF Core tarafında track edilen bir entity’de güncelleme yapıldığında tekrar update() methodunun çağrılmasına ihtiyaç yoktur.

DbContext — Asenkron ve Senkron Metotlar 🏎️

Soru 2: Entity Framework tarafında Add() metodunun senkron (sync) ve asenkron (async) versiyonları bulunurken, Remove() ve Update() metotlarının neden asenkron (async) versiyonları yoktur?

Add işlemi update ve remove işlemlerine göre daha maliyetli bir işlemdir. Dolayısıyla thread’in bloklanmaması için framework Add metodunun asenkron olaran AddAsync() metodunu bizlere sunmuştur.

Update ve delete işlemi ise maliyetli bir işlem olarak görülmez. Çünkü ilgili entity ya da entitylerin sadece durumu (state) değişir. Fakat Add işleminde ilgili verinin belleğe (memory)’e eklenme süreci olduğu için asenkron (async) bir metotu bulunmaktadır.

Asenkron metotları, threadlerin bloklanmaması ve thread’leri efektif şekilde kullanılması için tercih edilmelidir.

Entity Konfigürasyon ⚙️

Mevcut entity sınıflarınızı konfigüre edebilmek için birden fazla yöntem bulunmaktadır. Bu yöntemleri aşağıdaki tabloda görüntüleyebiliriz.

Fluent API — Entity Configuration — EntityFramework Core

Fluent API ❤️ [fluent design pattern]

Veri model konfigürasyonu yapmanın alternatif ve gelişmiş bir yoludur. İsmi Fluent Design Pattern’dan gelmekte olup amacı entityleri konfigüre ederken arka arkaya metotlar çağırabileceğimiz anlamına gelmektedir.

IEntityConfiguration — DbContext — EntityFramework Core

11. satırda IsFixedLength(true) metoduyla birlikte ilgili kolonun minimum ve maksimum değerini 5 olarak tanımlamış olduk.

nchar/varchar — SQL Table — EntityFramework Core

nchar(5) tipi ile kolonun sabit olarak 5 karakter olacağını bildiririz. nvarchar(200) var ifadesiyle sabit olmayacağını 0 ile 100 karakter arasında olabileceğini bildiririz.

Data Annotations Attributes

Fluent API ile sağlanan konfigürasyonların bir kısmını attributeler yardımıyla da yapabiliriz. Fakat best practices açısından uygun değildir.

EF Core konfigürasyon kodlarını dikkate alırken öncelik sıralamasını 📃 şu şekilde tutar:

Fluent API > Data Annotations Attributes > Convensions

Data Annotations — Entity Configuration — EntityFramework Core

Yukarıdaki gist, Fluent API ile konfigüre edilen entity’nin data annotations attributes yardımıyla nasıl konfigüre edileceğinin bir göstergesidir.

[Column(Order = X)] : Mevcut tabloda sıralama yapılamaz. Attribute, ilk kez oluşturulan tablolar için kolonların sıralamasında kullanılmaktadır.

İlişkiler🖇[ one-one & one-many & many-many ]

1–1 (one-to-one) ilişkilerde ParentId property’si Child tabloda tanımlanması daha uygundur. Örneğin User-UserDetail tabloları arasında User tablosuna UserDetailId propertysini tanımlarsam ileride sorun yaşamam olasıdır. Çünkü User eklerken UserDetail eklemek istemeyebilirim. Dolayısıyla User tablosunda UserDetailId property’si varsa sorun çıkaracaktır. Aşağıdaki gibi bir 1–1 ilişki tanımlaması uygun olacaktır.

One to One — Relationships — EntityFramework Core

N-N (many-to-many) ilişkilerde mutlaka arada bir tablo olması gereklidir. İki tablo arasında n-n bir ilişki kurulamaz. Dolayısıyla n-n bir ilişki kurmak için iki tane 1-N tablo oluşturmak gereklidir.

Many To Many— Relationships — EntityFramework Core
Many To Many— SQL Server — EntityFramework Core

İlişkilerdeki Silme Davranışları

Cascade (Kademeli): Varsayılan davranıştır ve tehlikelidir. Örneğin, kategori silinirse kategoriye ait ürünlerde silinecektir.

Restrict (Kısıtlamak): Kısıtlayıcıdır. Örneğin, silmek istediğiniz kategoriye ait ürünler varsa kategorinin silinmesini engeller.

SetNull (Null Ayarla): Örneğin, ürün tablosundaki referans property olan CategoryId <Nullable> olduğunu ve ilişki tanımında da OnDelete(DeleteBehavior.SetNull) olarak tanımlandığını düşünelim. Bu senaryo da kategoriyi sildiğinizde kategoriye ait olan ürünlerin categoryId property’si null olarak set edilecektir. Bu davranışta ForeignKey sütununun mutlaka nullable olması gerekmektedir.

SetNull — Delete Behaviour — EntityFramework Core

Seat markasını sildiğimde, seat markasına ait BrandId sütununu NULL olarak güncellendi. Çünkü DeleteBehaviour.SetNull olarak belirledik.

NoAction (Hiçbir Eylem): EF Core’a silme işlemlerinde aksiyon almamasını belirtiyoruz. Buradaki davranışı, veri tabanı tarafında ne şekilde belirlersek (default olarak cascade davranış belirlenir) ona göre hareket edilecektir.

Yukarıdaki enum değerleri haricinde, bir de ClientCascade, ClientNoAction, ClientSetNull gibi Client ile başlayan enum değerleri mevcuttur. Eğer kullanmış olduğunuz ilişkisel veri tabanı (RDBMS) Cascase, Restrict veya SetNull gibi özelliklere sahip değilse bu özellikleri EF Core tarafına kazandırmak isteyebilirsiniz. Yani veri tabanının böyle bir özelliği yok ancak bu özellikleri EF Core tarafında kazandırmak istiyorsak Client ile başlayan enum değerlerini kullanmalıyız.

Virtual OnModelCreating —DbContext — EntityFramework Core

Hatırlatma: Her DbContext örneğinde virtual metot olan OnModelCreating() metodu çalışmaktadır.

DatabaseGenerated Attribute

DatabaseGeneratedOption.[None]: Veri tabanı tarafından otomatik değer üretmeyi kapatır.

DatabaseGeneratedOption.[Computed]: EF Core insert/update işlemlerinde ilgili property’i sorgulara dahil etmez.

DatabaseGeneratedOption.[Identity]: EF Core sadece update işlemlerinde bu alanı dahil etmez.

DatabaseGeneratedOption — EntityConfiguration — EntityFramework Core

Farklı senaryolarda tablonuzdaki bazı sütunların değerlerini veri tabanınden set etmeniz gerekebilir. Örneğin public DateTime CreatedDate {get; set;} bu tarih propertysini EF Core tarafından (kodlama tarafından) değil de SQL Server ile tablodaki ilgili sütun için default tarih belirleyebiliriz. Dolayısıyla sütunun ekleme/güncelleme olayını veri tabanı tarafından gerçekleştiriyorsak, DatabaseGenerated Attribute’unu kullanmamız gereklidir.

Fluent API / DatabaseGeneratedOption — EntityConfiguration — EntityFramework Core

[None] (Hiçbiri): İlgili property için otomatik artan bir değer üretilmeyecektir. Genel olarak primary key (Pk_Id) alanını 1–1 artan değer belirlenir. Bazı senaryolarda sütunu primary key olarak belirleyip, otomatik olarak artmasını istemeyebilirsiniz.

[Computed] (Hesaplanmış): Sütunun ekleme/güncelleme işlemini veri tabanından yapıldığını bildiririz. EF Core’a ilgili property’i ekleme/güncelleme işlemlerinde sorguya dahil etmemesini sağlarız.

[Identity] (Kimlik): Sütunun sadece ekleme işlemini veri tabanından yapıldığını bildiririz. Şöyle ki, bazı sütunları sadece ekleme esnasında set ediyor olabilirsiniz. Örneğin CreatedDate. public DateTime CreatedDate { get; set; } . Bu gibi property’leri ister veri tabanından default value ile, istersek de kodlama esnasında set edebiliriz. Özetle bir alanı sadece insert edildiği anda doldurmak istiyorsak [DatabaseGenerated(DatabaseGeneratedOption.Identity)] attribute’undan faydalanmalıyız. Dolayısıyla EF Core sadece insert işleminde ilgili propertyi sorguya dahil ediyor. Çünkü CreatedDatesadece insert işleminde gönderilir, update işlemlerinde CreatedDategönderilmesine gerek yoktur.

İlişkili Data Yüklemeleri (Related Data Load)

Eager (İstekli) Loading: Veri tabanından veriyi listelerken aynı anda ilişkili olduğu veriyi de listelediğiniz türdür. Örneğin veri tabanından kategoriyi alırken o kategoriye ait ürünleri de beraberinde almak isteyebilirsiniz.

var categories = _context.Categories.Include(p => p.Products).ToList();

Explicit (Açık) Loading: Entity’i elde ettikten sonra belirli koşullar sağlanırsa entity’nin ilişkili olduğu diğer entity’leri de listelemek istediğiniz türdür. Örneğin veri tabanından kullanıcıyı listelediniz ve belirli business kurallardan sonra kullanıcıya ait detayları (UserDetail) ve adresleri de (Addresses) listelemek istiyorsunuz. Mümkün mü? Tabikii de.

Gist üzerinden de anlaşılacağı üzere explicit loading üzerinde Navigation Property,

  • Class tipinde ise Reference() metodunu → 7. Satır
  • IEnumerable tipinde ise Collection() metodunu → 12. Satır

kullanıyoruz.

Table Per Type (TPT) ve Table Per Hierarchy (TPH)

Konu, EF Core’un kalıtımsal durumlardaki davranışlarından ortaya çıkmaktadır.

Table Per Type(TPT), tablodaki belirli kolonların (bu kolonlar genelde ortak olan kolonlardır.) 1–1 ilişkili olarak farklı tablolarda tutulmasıdır.

Table Per Hierarchy(TPH), hiyerarşi içindeki tüm entity sınıfları için tek bir tablo oluşturulmaktadır. Bu davranışta EF Core verinin hangi tabloya ait olduğunu ayrıştırabilmek adına discriminator kolonunu varsayılan olarak tabloya eklemektedir.

Keyless Entity Types — [Keyless]

Primary key kolonuna sahip olmayan entity/tablolarda Insert/Update/Delete işlemleri gerçekleştiremezsiniz. Aksi takdir de aşağıdaki hatayla karşılaşmanız olasıdır.

System.InvalidOperationException: ‘Unable to track an instance of type ‘{EntityName}’ because it does not have a primary key. Only entity types with a primary key may be tracked.’

Primary key sütunu olmayan bir entity’nin aşağıdaki şekilde konfigüre edilmesi uygundur.

  • Key tanımlı değildir ve DbContext tarafından track edilmezler.
  • Raw Sql Cümleciklerinden dönen veriyi maplemek için kullanılabilir.
  • Primary Key (Pk_Id) ‘ye sahip olmayan view veya tabloları maplemek için kullanılabilir.

BONUS

EF Core tarafında yazdığımız kodların çıktısı olan SQL sorgularına TagWith metoduyla birlikte yorum satırı ekleyebilirsiniz.

_context.Users
.TagWith("Bu sorgu yaşı 18 ve üstü kullanıcıları listeler.")
.Where(user => user.Age >= 18)
.ToList();

Yukarıdaki kodun SQL Server Profiler aracılığıyla çıktısı aşağıdaki gibidir.

Düşündüğünüzde, TagWith metotunun bizlere birçok yardımı olacaktır. Bir başkası tarafından yazılan sorgunun ne amaçla yazıldığını ilk bakışta anlayabilir veya SQL tarafına gönderilen sorgulardan kolayca ayırt etme işlemi yapabiliriz.

--

--