Teknik Muhabbetler #4 (Entity Framework Core & ORM)

Furkan Güngör
Oct 6, 2020 · 7 min read

Bir sprint daha geçiyor ömründen heeey geliştirici, meslek hayatında anlatabileceğin, tekrar yapmamak için dikkat edeceğin bir düzine daha hata yaptığın, bu sprint içerisinde neler öğrendin? Emanetçi olduğun bu metotların kaçını refactor ettin diye sormazlar mı? Emin olun sorarlar. 😃

Günlük hayatta bir çoğumuz sprintler koşuyoruz, sprint planlanırken takım; nereye koşacağını öğreniyor ve takım çantasına alması gereken aletleri alıyor, veeee düdük çaldı yarış başladı…

Bu kadar yoğun sprintler koşarken devam ettirmek için uğraştığım bu teknik seri benim için bir terapi oluyor. 😃 Umarım sizler de benimle aynı duyguları yaşıyorsunuzdur.

Teknik muhabbetler serisinin bu yazısında Mobiroller içerisinde ağırlıklı olarak kullandığımız EF Core’u inceleyip derin bir dalış gerçekleştirmeye çalışacağım.

ORM (Object Relational Mapping)

ORM bir programlama tekniğidir, bir tool değildir. Bazı ORM araçları;

  • Entity Framework
  • Entity Framework Core
  • Dapper
  • nHibernate

Projenin işleyişi ile alakalı tercih edeceğimiz bu araçlar bazen değişkenlik gösteribiliyor. Bazı projelerde EF Core kullanırken bazı projelerde Dapper veya hibrit bir model kullanabiliyoruz. Hepsinin birbirine göre avantajları mevcut ancak bu yazının amacı EF Core aracını derinlemesine incelemek olduğu için cümlemi burada bitiriyorum. 😃

Packages;Install-Package Microsoft.EntityFrameworkCore -Version 5.0.0-rc.1.20451.13
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 5.0.0-rc.1.20451.13
Install-Package Microsoft.EntityFrameworkCore.Tools -Version 5.0.0-rc.1.20451.13
Install-Package Microsoft.Extensions.Logging -Version 5.0.0-rc.1.20451.14

İncelemeleri Customer ve Order varlıkları üzerinde gerçekleştireceğim.

Tüm varlıklar üzerinde ortak kullandığım parametreleri BaseEntity üzerinde topladım.

Hazırlıklar tamamlandığına göre yolculuk başlasın. 😃 🛫

Value Generator

Örneğin CreateDate , UpdateDate , IsActive alanlarını en ilkel yöntem ile Insert ve Update işlemlerinde uygulayalım.

// Update
var customer = dbContext.Customers.First();
customer.CreateDate = DateTime.UtcNow;
customer.UpdateDate = DateTime.UtcNow;
customer.IsActive = false;

Evet bu yöntem kesinlikle çalışır ancak her entity için böyle bir logic yazmak kodun okunabilirliğini ve temiz kalmasını negatif yönde etkileyecektir.

Yukarıda belirttiğim senaryoya ek olarak yeni bir modül ekleyelim. Order varlığı üzerinde OrderCode isimli bir alanımız var. Yeni sipariş verildiğinde MBR ile başlayan yeni bir OrderCode verilmesini sağlayalım.

var order = new Order(){OrderCode = "MBR"+Guid.NewGuid().ToString()}

Ek olarak daha farklı senaryolar eklenebilir, bu durumda artan senaryoları tek bir yerden kontrol etmek ve uygulamanızı sadeleştirmek için value generator yapılarını kullanabilirsiniz.

DbContext OnModelCreating içerisinde bu alanları işaretlemeniz yeterli olacaktır.

Ne Zaman Insert? Ne Zaman Bulk Insert?

Bu konuyu özetlemek için Mobiroller’da yaşadığımız günlük problemlerden bahsedeceğim.

Mobiroller içerisinde microservice dönüşümü gerçekleştirdiğimiz için monolith uygulamadan kopardığımız yapıları microservice mimarisinde kurguluyoruz. Genelde ilk sorduğumuz soru “Bu domain için hangi database kullanmalıyız?” oluyor. Sık sık veri tabanı değişikliği yaptığımız için data migration artık rutinlerimizden biri haline geldi. Bu sebeple Insert ve Bulk Insert farkı Mobiroller için çok önemli.

Uzun lafın kısası 😃 Eğer eklediğiniz kayıt sayısı 0 ile 3 arasında ise Entity Framework Core kayıt sayısı kadar Insert komutu üretirken, kayıt sayısının daha fazla olduğu durumlarda tek bir Insert komutu üretmektedir.

Oluşan SQL sorgusunu inceleyelim;

Microsoft.EntityFrameworkCore.Database.Command: Information: Executed DbCommand (73ms) [Parameters=[@p0='?' (DbType = Guid), @p1='?' (DbType = DateTime2), @p2='?' (DbType = Boolean), @p3='?' (Size = 200), @p4='?' (Size = 11), @p5='?' (Size = 600), @p6='?' (DbType = DateTime2), @p7='?' (DbType = Guid), @p8='?' (DbType = DateTime2), @p9='?' (DbType = Boolean), @p10='?' (Size = 200), @p11='?' (Size = 11), @p12='?' (Size = 600), @p13='?' (DbType = DateTime2), @p14='?' (DbType = Guid), @p15='?' (DbType = DateTime2), @p16='?' (DbType = Boolean), @p17='?' (Size = 200), @p18='?' (Size = 11), @p19='?' (Size = 600), @p20='?' (DbType = DateTime2), @p21='?' (DbType = Guid), @p22='?' (DbType = DateTime2), @p23='?' (DbType = Boolean), @p24='?' (Size = 200), @p25='?' (Size = 11), @p26='?' (Size = 600), @p27='?' (DbType = DateTime2)], CommandType='Text', CommandTimeout='30']SET NOCOUNT ON;INSERT INTO [Customers] ([CustomerId], [CreateDate], [IsActive], [Name], [PhoneNumber], [Surname], [UpdateDate])VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6),(@p7, @p8, @p9, @p10, @p11, @p12, @p13),(@p14, @p15, @p16, @p17, @p18, @p19, @p20),(@p21, @p22, @p23, @p24, @p25, @p26, @p27);

Migration

Bu sorunu yaşamamak için Startup.cs içerisine küçük bir kod ekleyebilirsiniz. 😃

  • Migrate : Context’in son haline bakarak bekleyen migration var ise uygular.
  • EnsureDeleted : Veri tabanını siler.
  • EnsureCreated : Veri tabanı varsa herhangi bir işlem yapmaz eğer veri tabanı yoksa son migration’ı uygulayarak veri tabanını oluşturur.

Keyless Entity

Keyless niteliği(attribute) ile varlığınızın(entity) birincil anahtarının olmadığını belirtebilirsiniz.

View‘ler sorguları basitleştirmek, erişim izinlerini düzenlemek, farklı sunuculardaki eşdeğer verileri karşılaştırmak veya bazı durumlarda sorgu süresini kısaltmak için kullanılan, gerçekte olmayan Select ifadeleri ile tanımlanmış sanal tablolardır. İçerisinde veri bulunmaz. Sadece tabloların görünümleridir. Kullanıcıların bazı kritik tabloların sadece belli sütunlarını veya satırlarını göstermesi gerektiği durumlarda kullanılabilir.

Connection Resiliency

Acaba Ne Üretti? 😃(ToQueryString)

SELECT [c].[CustomerId], [c].[CreateDate], [c].[IsActive], [c].[Name], [c].[PhoneNumber], [c].[Surname], [c].[UpdateDate]
FROM [Customers] AS [c]
WHERE [c].[Name] = N'customer'

DbContext SetConnectionString

Include İçerisine Condition Yazmasak Mı? 😃 💻

Split Query ➗

EF Core 5 ile birlikte LINQ sorguları birden fazla SQL komutu oluşturabilir. Peki neden? 🤔
EF Core 3 tüm LINQ sorgularını tek bir SQL komutu şeklinde çalıştırır. Tahmin ettiğiniz gibi join, include gibi ağır sorgularda bu yöntem yerine farklı SQL komutları oluşturarak performans artışı sağlanmak istenmiş.

SELECT [c].[CustomerId], [c].[CreateDate], [c].[IsActive], [c].[Name], [c].[PhoneNumber], [c].[Surname], [c].[UpdateDate]FROM [Customers] AS [c]ORDER BY [c].[CustomerId]SELECT [t0].[OrderId], [t0].[CreateDate], [t0].[CustomerId], [t0].[IsActive], [t0].[IsShipped], [t0].[OrderCode], [t0].[Total], [t0].[UpdateDate], [c].[CustomerId]FROM [Customers] AS [c]INNER JOIN (SELECT [t].[OrderId], [t].[CreateDate], [t].[CustomerId], [t].[IsActive], [t].[IsShipped], [t].[OrderCode], [t].[Total], [t].[UpdateDate]FROM (SELECT [o].[OrderId], [o].[CreateDate], [o].[CustomerId], [o].[IsActive], [o].[IsShipped], [o].[OrderCode], [o].[Total], [o].[UpdateDate], ROW_NUMBER() OVER(PARTITION BY [o].[CustomerId] ORDER BY [o].[OrderId]) AS [row]FROM [Orders] AS [o]) AS [t]WHERE [t].[row] <= 5) AS [t0] ON [c].[CustomerId] = [t0].[CustomerId]ORDER BY [c].[CustomerId], [t0].[CustomerId], [t0].[OrderId]

Interception 🤙

İlerleyen zamanlarda Interception özelliğini kullanarak bir tool yazmayı planlıyorum. Belki sen bu yazıyı okuduğunda ben o kütüphaneyi yazmışımdır. Kontrol et. 😃 💻

SELECT [c].[CustomerId], [c].[CreateDate], [c].[IsActive], [c].[Name], [c].[PhoneNumber], [c].[Surname], [c].[UpdateDate]FROM [Customers] AS [c] order by 1 desc

Global Query Filters 🌎

Entity üzerinde sorgularınızın vazgeçilmez parametreleri olduğunda Global Query Filter kullanabilirsiniz. Biraz daha developer friendly inceleyelim.

Örneğin Customer varlığı üzerinde her zaman son 30 günde aktif olan kayıtlar üzerinde işlem yaptığımızı düşünelim.

SELECT [c].[CustomerId], [c].[CreateDate], [c].[IsActive], [c].[Name], [c].[PhoneNumber], [c].[Surname], [c].[UpdateDate]FROM [Customers] AS [c]WHERE ([c].[IsActive] = CAST(1 AS bit)) AND ([c].[CreateDate] > DATEADD(day, CAST(-30.0E0 AS int), GETUTCDATE())) order by 1 desc

Eğer global filtrenin uygulanmasını istemiyorsanız;

var dataIgnoreGlobalFilter = dbContext.Customers.IgnoreQueryFilters().ToList();SELECT [c].[CustomerId], [c].[CreateDate], [c].[IsActive], [c].[Name], [c].[PhoneNumber], [c].[Surname], [c].[UpdateDate]FROM [Customers] AS [c] order by 1 desc

Sanırım yazacaklarım bitti. 😃 Ama eksiklerimi görürsen lütfen bana bildir sevgili developer. 😏

Yazdığınız kod prod ortamlarında refactor edilmeden koşsun. 💻 😃

Happy Coding. 😃

Kaynaklar

Mobiroller Tech

Mobiroller platformunu geliştiren ekip olarak üzerinde…

Mobiroller Tech

Mobiroller platformunu geliştiren ekip olarak üzerinde çalıştığımız problemleri, ilginç konuları ve hikayelerimizi burada paylaşıyoruz.

Furkan Güngör

Written by

Solution Developer — I want to change the world, give me the source code.

Mobiroller Tech

Mobiroller platformunu geliştiren ekip olarak üzerinde çalıştığımız problemleri, ilginç konuları ve hikayelerimizi burada paylaşıyoruz.