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

Kısıtlamalar, bir veya daha fazla sütunun değerlerinin sınırlandırılması için kullanılır.

Cihat Solak
Intertech

--

Entity Framework — Entity Framework Core — ORM

nvarchar & varchar — Entity Properties

Genellikle string ifadeler nvarchar olarak kaydedilir. Yani içerisinde UTF-8 formatında Türkçe de dahil tüm karakterleri kaydedebileceğimiz formattır. Fakat yer yer URL, IBAN gibi sadece ASCII karakterlerini içeren ifadeleri saklayacak olabilirsiniz. Dolayısıyla bu gibi durumlarda varchar tipini kullanmalıyız. Neden? 🙄

Çünkü nvarchar her bir karaktere karşılık 2 BYTE yer ayırırken varchar her bir karaktere karşılık 1 BYTE yer ayırır. Dolayısıyla depolamadan 🗃️tasarruf etmek için uygun yerlerdevarchar tercih etmelisiniz.

Entity içerisinde bulunan property’nin veri tabanında karşılığı yoksa ya da veri tabanına yansıtmak istemiyorsanız modelBuilder.Entity<T>().Ignore(x => x.Property); şemasını kullanmalısınız.

Index — Entity Configuration

İlişkisel veri tabanlarında sorgulama performansına önemli derece de etki eden araçlardan biridir. Genellikle fazlaca sorguladığınız sütunlarda indexleme 💫 yapmanız önerilmektedir.

modelBuilder.Entity<T>().HasIndex(p => p.Name);

Composite Index: Birden fazla sütunun birleştirilerek oluşturulduğu ve veri tabanı sorgularının daha hızlı çalışmasını sağlayan bir veri yapısıdır. Yani tablo üzerinde tanımlanan index tek kolon üzerinden değil de birden fazla kolon üzerinden tanımlanmasıdır. 🤸‍♂️

modelBuilder.Entity<T>().HasIndex(p => new { p.Name, p.Age } );

Çok fazla sorguladığınız sütunlarda doğru indexleme yapmak önemlidir. Çünkü veri tabanı ilgili alanlarda tüm tabloyu taramak 🔎 yerine index tablosunu kullanarak bu işlemi gerçekleştirir.

10. satırda yer alan bir sorgulamaya çokça sahipseniz, 17. satırda yer alan indexleme yönteminden faydalanmayı düşünebilirsiniz. Böylece ilgili index ile birlikte kullanıcıları index tablosunda tutarken aynı zamanda adreste index tablosunda tutulacaktır. Dolayısıyla adres alanını alabilmek için asıl tabloya gitmeyecek, direkt olarak index tablosundan veriler listelenecektir. Bu işlem sorgu performansını 🚀 artacaktır. Dezavantajı ise SQL Server’ın kullandığı storage alanının artacağıdır. (Ekstra koşullar göz ardı edilmiştir.)

Index oluşturduğunuzda sorgularınızı da indexlere göre yazarsanız en yüksek performansı elde etmiş olursunuz.

https://l24.im/Epo

Constraints (Kısıtlamalar) — Entity Configuration

Veri tabanının tutarlılığını arttırmaya yönelik bir özelliktir. Kısıtlama şartlarına uymayan veri, kayıt edilmek istenirse hata ile karşılaşılacaktır. Örneğin ürünün indirimli fiyatı, satış fiyatından düşük olmalıdır. Veri bütünlüğü göz önüne alındığında indirimli fiyatın, satış fiyatından yüksek olduğu bir senaryoyu veri tabanına yansıtmamalıyız.

Constraints (Kısıtlamalar) — Entity Configuration

Bu tür kısıtlamalar EF Core’un özelliği değil, veri tabanının sağladığı özelliktir.

EF Core Veri Sorgulama (Querying Data)

EF Core where(predicate) metodu içerisinde yazılan sorguyu direkt olarak SQL cümleciğine dönüştürür. Dolayısıyla where ifadesi içerisinde özelleştirilmiş metotlar kullanamayız, buna izin verilmez.

EF Core Veri Sorgulama (Querying Data)

Yukarıdaki örnekte olduğu gibi gömlekler üzerinde formatlayarak sorgulama yapmak istiyorsak, veriyi belleğe aldıktan sonra bu işlemi yapmalıyız. [4. satır]

EF Core özel (custom) metotları SQL sorgularına dönüştüremez.

JOINS (Inner, Left/Right, Full Outer)

Join Query In Entity Framework

Tablolar arasında ilişki (navigation property) bulunmadığında, tabloları birbirine bağlayabilmek için join kullanırız.

Inner Join

Lambda Expression veya LINQ ile yazılmış kodun SQL çıktısı aynıdır. Şahsen join işlemlerinde LINQ’nun daha sade olduğu kanaatindeyim.

Left & Right Join

Full Outer Join

EF Core tarafında desteği yoktur. Left join sorgusu ile right join sorgusu yazıp, her iki sonuç kümesini birleştirerek full outer join elde edibilir.

Store Procedure (SP)

Parametre alabilen geriye değer döndüren ya da döndürmeyen alt ♟️ programlardır.

  • ExecuteSqlInterpolated: Update, Insert, Delete
  • FromSqlInterpolated: Get

Store Procedure İle Out Parametresini Yakalama

Function (User Defined Function)

İstenilen değer tipinde geri dönüş yapabilen SQL işlevidir.

Table-valued function, geriye tablo döndüren fonksiyonlardır.

EF Core tarafında (kodlamada) Function’larda WHERE koşulu SQL cümleciğine yansır fakat Store Procedure’lerde where koşulu kullanılamaz.

Yukarıdaki örnekte function’ları ef core tarafına tanıtmanın alternatif ikinci yolunu görüyoruz. Burada gözden kaçmaması gereken 🌟 nokta function’dan sonra tanımlamış olduğumuz where koşulununda SQL cümleciğini eklenmiş olmasıdır. Biliyorsunuz ki store procedure (sp) tarafında bu mümkün değildir.

Scalar-valued function, geriye bir tek değer döndüren fonksiyonlardır.

Function İle Store Procedure Farkları

🔸 Function içerisinde sadece ve sadece select kullanılabilir, update veya delete işlemi yapılamaz. Stored procedure ise crud (create/read/update/delete) işlemlerin tamamı yapılabilir.

🔸 Fonksiyonlarda sadece giriş parametresi vardır. Store procedure’lerde ise hem giriş (input) hemde çıkış (output) parametresi vardır.

🔸 Function transaction yapısını desteklemez fakat stored procedure destekler.

PROJECTIONS 📽️

🎁 Süpriz 1

Genellikle veri tabanından belirli alanları almak istediğimizde LINQ’nun bizlere sağlamış olduğu Select(Expression<Func<TSource, TResult>> selector) metodundan faydalanırız. Dilerseniz bu işlemi AutoMapper paketinin sağlamış olduğu ProjectTo<TDestinition> generic metotuyla da yapabilirsiniz.

public class ProjectionsController : ControllerBase
{
private readonly FinanceDbContext _context;
private readonly IMapper _mapper;

public ProjectionsController(FinanceDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}

[HttpGet]
public IActionResult AutoMapper()
{
var shirts = _context.Shirts.Select(p => new Shirt
{
Name = p.Name,
Stock = p.Stock,
SalesPrice = p.SalesPrice
}).ToList();

var shirtDtos = _context.Shirts.ProjectTo<ShirtDto>(_mapper.ConfigurationProvider).ToList();

return Ok();
}
}

🎁 Süpriz 2

Genellikle ilişki durumundaki entitylerde sorgulama yaparken Include ile navigation property’i sorguya dahil ederiz. Buna ihtiyaç yoktur!

//1. Yöntem
_context.Users
.Include(p => p.UserDetail)
.Select(p => new
{
p.Name,
p.UserDetail.PhoneNumber //One-To-One
});

//2. Yöntem
_context.Users.Select(p => new
{
p.Name,
p.UserDetail.PhoneNumber //One-To-One
});

2. Yöntemde gördüğünüz üzere Include metotunu kullanmadan p.UserDetail.PhoneNumber erişim sağlayabilirim. Özetle Linq’nun sağlamış olduğu select metodunda navigation propertylere erişecekseniz Include, ThenInclude metotlarını kullanmanıza gerek yoktur. EF Core bu işlemleri kendisi halledecektir.

Her iki kullanımında SQL karşılığı olarak birbirinden farkı bulunmamaktadır. 💁‍♂️

TRANSACTION

Entity Framework, yaptığımız tüm işleri SaveChanges() metotunu çağırana kadar biriktiriyor 📦 (paketliyor) ve en son veri tabanına yansıtıyor. Nasıl mı?

Post post = new()
{
Title = "",
Content = ""
};

_context.Posts.Add(post); // 1 - Post Ekleme İşlemi

Tag tag = _context.Tags.First();
tag.Name = $"[Updated] {tag.Name}"; // 2 - Tag Güncelleme İşlemi

_context.SaveChanges();

return Ok();

Örneğin post eklendiğini fakat tag’in adını güncelleyemediğini (hata aldığını) varsayalım. Bu durumda tüm işlemler rollback olacak, post’da veri tabanına eklenmeyecektir. SaveChanges() metodu ile birlikte EF Core transaction sağlamaktadır, dahili transaction yapısı framework tarafından sunulmuştur.

Eğer 1 adet SaveChanges() metotu kullanılıyorsa, transaction metotu kullanımına ihtiyaç yoktur. ❗❗

İlişki olan iki tablo (one-one, one-many, many-many) varsa burada açık bir şekilde transaction kullanımına gerek yoktur. Çünkü tablolara arasında ilişki olduğu için gizli bir transaction ❗❗ bulunmaktadır.

UserDetail userDetail = new()
{
PhoneNumber = "5000000000"
};

_context.UserDetails.Add(userDetail);

User user = new()
{
Name = "Cihat",
Age = 1,
UserDetail = userDetail
};

_context.Users.Add(user);
_context.SaveChanges();

Transaction kullanımında try-catch kullanmak zorunda değiliz ⚠️ ve try-catch kullanımında kesinlikle catch bloğunda rollback yapısını kurgulayınız. Yukarıdaki örnekte UserDetail ya da User ekleme işleminde hata alınırsa transaction varlığından dolayı tüm işlemler ef tarafından rollback edilecektir.

Transaction işlemi sırasında ilgili veriler, veri tabanına yansımış gibi davranır fakat veri tabanına Commit() metodunu kullanmadan yansımaz.

using var transaction = _context.Database.BeginTransaction();

UserDetail userDetail = new()
{
PhoneNumber = "5000000000"
};

_context.UserDetails.Add(userDetail);
_context.SaveChanges(); // SaveChanges()

User user = new()
{
Name = "Cihat",
Age = 1,
UserDetail = userDetail
};

_context.Users.Add(user);
_context.SaveChanges(); // 2.kez SaveChanges()

transaction.Commit(); // Transaction Commit

Görüldüğü gibi transaction'ı try-catch blokları içerisine alıp açık açık rollback mekanizması kurmadık. Çünkü ihtiyacımız bulunmamaktadır. Bu kısım 🍫 çokomelli!

ÇOKLU DBCONTEXT KULLANIMI (MULTIPLE DBCONTEXT INSTANCE)

Çoğunlukla projelerde tek bir DbContext instace’ı ile çalışırız. Bu instance varsayılan olarak scoped yaşam 🧬döngüsüne sahiptir. Yani ilgili request response’a dönüşünceye kadar aynı instance üzerinden işlemler devam edecektir.

Peki soru geliyor... Hazır mıyız? İki farklı DbContext instance’ları arasında transaction nasıl sağlarız? (Context sınıfı aynı olmalı)

DbConnection appDbConnection = new SqlConnection(Initializer.Configuration.GetConnectionString("SqlCon"));

using var _appDbContext1 = new AppDbContext(appDbConnection);
using var appDbTransaction1 = _appDbContext1.Database.BeginTransaction();

_appDbContext1.Brands.Add(new Brand
{
Name = "Transaction Model"
});

_appDbContext1.SaveChanges();

using var _appDbContext2 = new AppDbContext(appDbConnection);
_appDbContext2.Database.UseTransaction(appDbTransaction1.GetDbTransaction()); // Transaction paylaşımı

var course = _appDbContext2.Students.First();
course.Name = "Transaction Mesut";

_appDbContext2.Students.Update(course);
_appDbContext2.SaveChanges();

appDbTransaction1.Commit();

Bis bald! 🤠

--

--