İçeriğe geç
Educora
İleri24 dk15 / 16

Entity Framework Core: veritabanlarıyla çalışmak

EF Core ile sınıflardan tablolar oluştur: `DbContext`, migration'lar, LINQ to Entities, CRUD işlemleri, ilişkiler ve tipik performans tuzakları.

Kendini test et
Bu derste öğreneceklerin
  • Model sınıfları ve bir DbContext yazmak, bir migration oluşturup uygulamak
  • LINQ ile veri okumak, eklemek, değiştirmek ve silmek
  • İlişkileri kurmak, N+1 sorunundan ve SQL enjeksiyonundan kaçınmak

Alt düzey ADO.NET ile her SQL sorgusunu elle yazman ve her sütunu bir nesneye aktarman gerekir. .NET'in resmî ORM'i olan Entity Framework Core (EF Core) bu işi üstlenir: sınıflarını yazarsın, EF Core onlar için tablolar oluşturur, LINQ sorgularını SQL'e çevirir ve sonuçları nesnelere toplar. EF Core; SQL Server, PostgreSQL, MySQL ve SQLite ile çalışır; örneklerde kurulum gerektirmeyen SQLite'ı kullanacağız.

  1. 1
    Paketleri ekle

    Microsoft.EntityFrameworkCore.Sqlite SQLite sağlayıcısıdır, Microsoft.EntityFrameworkCore.Design ise migration araçları için gerekir.

  2. 2
    dotnet-ef aracını kur

    dotnet tool install --global dotnet-ef, dotnet ef komutlarını ekler; bunu bilgisayarda bir kez yapmak yeterlidir.

  3. 3
    Modeli ve DbContext'i yaz

    Her tablo bir sınıftır; DbContext ise veritabanına açılan “kapı”dır.

  4. 4
    Bir migration oluştur ve uygula

    dotnet ef migrations add InitialCreate şemayı bir C# dosyası olarak yazar, dotnet ef database update ise onu veritabanına uygular.

Terminal
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
Proje klasöründe çalıştırılır (paketler nuget.org'dan indirilir)

Model ve DbContext

EF Core geleneklerle çalışır: Id adlı özellik birincil anahtardır, ClassroomId + Classroom çifti bir yabancı anahtardır, List<Student> ise “bir sınıfta çok öğrenci vardır” ilişkisidir. required anahtar sözcüğü, Name'in NOT NULL bir sütun olacağını ve nesne oluşturulurken mutlaka verilmesi gerektiğini belirtir. DbContext'in DbSet<T> özellikleri tablolara karşılık gelir, OnConfiguring ise hangi veritabanına bağlanılacağını söyler.

C#
using Microsoft.EntityFrameworkCore;

public class Classroom
{
    public int Id { get; set; }                        // primary key by convention
    public required string Name { get; set; }
    public List<Student> Students { get; set; } = [];  // one classroom, many students
}

public class Student
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public int Score { get; set; }
    public int ClassroomId { get; set; }               // foreign key
    public Classroom Classroom { get; set; } = null!;
}

public class SchoolContext : DbContext
{
    public DbSet<Classroom> Classrooms => Set<Classroom>();
    public DbSet<Student> Students => Set<Student>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=school.db");
}
Model: iki sınıf, bir ilişki ve bağlam

Gerçek bir uygulamada bağlantı dizesi OnConfiguring içine yazılmaz: bağlam ayarlarını yapıcıdan alır (public class SchoolContext(DbContextOptions<SchoolContext> options) : DbContext(options)), dize ise bir yapılandırma dosyasından gelir (bunu bir sonraki derste ASP.NET Core'da göreceksin). DbContext hafif ve kısa ömürlü bir nesnedir: genellikle tek bir istek ya da tek bir iş birimi için oluşturulur ve iş parçacığı güvenli değildir; bu yüzden paralel görevler arasında paylaşılmamalıdır.

Migration'lar

Bir migration, modeldeki bir değişikliği veritabanına taşıyan bir C# dosyasıdır. migrations add modeli önceki durumuyla karşılaştırır ve Migrations/ klasöründe Up (uygula) ve Down (geri al) metotları olan bir sınıf oluşturur; database update ise henüz uygulanmamış migration'ları çalıştırır. Bir sınıfa yeni bir özellik mi ekledin? Yeni bir migration oluştur (örneğin AddEmail) ve veritabanını güncelle. Migration'lar Git'te tutulur; böylece ekipteki herkesin veritabanı aynı şemaya sahip olur.

Terminal
dotnet ef migrations add InitialCreate
dotnet ef database update
Beklenen çıktı
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
Build started...
Build succeeded.
Applying migration '20260927101500_InitialCreate'.
Done.
Örnek çıktı: migration adı bir tarih-saat damgasıyla başlar

LINQ to Entities: CRUD işlemleri

Bir DbSet<T> üzerinde yazılan LINQ sorgusu bellekte değil, veritabanında çalışır: EF Core ifade ağacını SQL'e çevirir (LINQ to Entities). Add ve Remove değişiklikleri yalnızca kaydeder, SaveChangesAsync ise onları tek bir işlemde veritabanına gönderir. Veritabanından okunan bir nesne izlenir (change tracking): özelliğini değiştirip SaveChangesAsync çağırmak yeterlidir; UPDATE komutunu EF Core kendisi yazar.

C#
await using var db = new SchoolContext();

var tenA = new Classroom { Name = "10A" };
tenA.Students.Add(new Student { Name = "Aysel", Score = 95 });
tenA.Students.Add(new Student { Name = "Leyla", Score = 88 });
db.Classrooms.Add(tenA);
await db.SaveChangesAsync();                         // INSERTs; Id values are filled in

List<string> top = await db.Students
    .Where(s => s.Score >= 90)
    .OrderByDescending(s => s.Score)
    .Select(s => s.Name)
    .ToListAsync();                                  // the SQL runs here
Console.WriteLine(string.Join(", ", top));

Student leyla = await db.Students.SingleAsync(s => s.Name == "Leyla");
leyla.Score = 91;                                    // the change tracker notices this
await db.SaveChangesAsync();                         // UPDATE "Students" SET "Score" = ...

db.Students.Remove(leyla);
await db.SaveChangesAsync();                         // DELETE
Console.WriteLine(await db.Students.CountAsync());
Beklenen çıktı
Aysel
1
Boş bir veritabanında örnek çıktı: 90+ puan yalnızca Aysel'de, Leyla silindikten sonra bir öğrenci kalıyor

EF Core'un hangi SQL'i ürettiğini görmek yararlıdır: ToQueryString(), sorguyu çalıştırmadan SQL metnini döndürür.

C#
var query = db.Students
    .Where(s => s.Score >= 80)
    .OrderByDescending(s => s.Score)
    .Select(s => s.Name);
Console.WriteLine(query.ToQueryString());
Beklenen çıktı
SELECT "s"."Name"
FROM "Students" AS "s"
WHERE "s"."Score" >= 80
ORDER BY "s"."Score" DESC
SQLite için örnek çıktı; diğer sağlayıcılar biraz farklı SQL üretir

İlişkiler, Include ve N+1 sorunu

İlişkili veriler otomatik olarak yüklenmez: student.Classroom ya da classroom.Students sorguda istenmediyse boş kalır. Include(...), ilişkili nesneleri aynı sorguda getirir. Çoktan çoğa bir ilişki için iki tarafa da bir koleksiyon koymak yeterlidir; ara tabloyu EF Core kendisi oluşturur. En bilinen performans hatası ise N+1'dir: liste için bir sorgu, ardından her eleman için bir sorgu daha.

C#
public class Course
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public List<Student> Students { get; set; } = [];
}

// in Student:  public List<Course> Courses { get; set; } = [];
// EF Core creates the join table CourseStudent automatically

var withCourses = await db.Students
    .Include(s => s.Courses)                 // load related data in the same query
    .Where(s => s.Courses.Any(c => c.Title == "Math"))
    .ToListAsync();
Çoktan çoğa ilişki: bir öğrencinin çok dersi, bir dersin de çok öğrencisi vardır
N+1: 1 + N sorgu
var classes = await db.Classrooms.ToListAsync();
foreach (var c in classes)
{
    // one more query for every classroom: the N+1 problem
    int count = await db.Students.CountAsync(s => s.ClassroomId == c.Id);
    Console.WriteLine($"{c.Name}: {count}");
}
İzdüşüm (projection): tek sorgu
var classes = await db.Classrooms
    .Select(c => new { c.Name, Count = c.Students.Count })
    .ToListAsync();                  // a single query with a subquery
foreach (var c in classes)
    Console.WriteLine($"{c.Name}: {c.Count}");
Tehlikeli: dize birleştirme
var list = await db.Students
    .FromSqlRaw("SELECT * FROM Students WHERE Name = '" + name + "'")
    .ToListAsync();                  // SQL injection!
Güvenli: interpolasyon bir parametreye dönüşür
var list = await db.Students
    .FromSql($"SELECT * FROM Students WHERE Name = {name}")
    .ToListAsync();                  // {name} becomes a SQL parameter
Ham SQL gerektiğinde FromSql (EF Core 7'den beri), interpolasyonla verilen değerleri SQL parametrelerine dönüştürür

Önemli noktalar

  • EF Core sınıfları tablolara eşler: DbContext + DbSet<T>; Id birincil anahtar, XxxId yabancı anahtardır.
  • Migration'lar: dotnet ef migrations add şemayı yazar, dotnet ef database update uygular; Git'te tutulur.
  • Bir DbSet üzerindeki LINQ, SQL'e dönüşür (ToQueryString() gösterir); değişiklikleri SaveChangesAsync kaydeder.
  • İlişkili verileri Include ya da bir izdüşümle tek sorguda yükle; N+1'den kaçın.
  • Ham SQL için FromSql($"..."); okuma için AsNoTracking(), toplu değişiklik için ExecuteUpdateAsync/ExecuteDeleteAsync.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Migration'ları veritabanına hangi komut uygular?