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.
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.
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.
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.
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)
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! 🤠