Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Entity Framework Core – Temporal Tables İle Çalışmak

Merhaba,

SQL Server 2016’da tanıtılmış olan Temporal Tables kavramı, veri değişikliği süreçlerinde kayıtları depolayan ve zaman içinde farklı noktalardaki tablo verilerinin analizi için kullanılan ve sistem tarafından yönetilen tabloları ifade etmektedir. EF Core, 6.0 sürümüyle birlikte temporal table’ları destekleyici nitelik kazanmıştır. Bu içeriğimizde, Code First yaklaşımı ile Temporal Tables’ların nasıl kullanılabileceğini inceleyecek ve LINQ eşliğinde geçmiş verileri nasıl sorgulayabileceğimize dair çalışma sergileyeceğiz.

Başlangıç

İlk olarak Temporal Table’ları inceleyebilmek için temel bir EF Core yapılanmasını herhangi bir console uygulaması üzerinden ayağa kaldıralım. Haliyle öncelikle aşağıdaki kütüphaneleri kurarak başlayalım :

Ardından basitlik ilkesi gereği örneklendirmeyi yalın bir şekilde sunabilmek için sadece aşağıdaki modeli üretmemiz yeterli olacaktır.

class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public PostType? PostType { get; set; }
}

enum PostType
{
    Talk,
    Side,
    Citation,
    Unknown
}

Şimdi yapılması gereken, bir context nesnesi oluşturmak ve yukarıda oluşturduğumuz modeli DbSet olarak context’te tanımlamak.

class TemporalExampleDb : DbContext
{
    public DbSet<Post> Posts { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=***;Database=TemporalExampleDb;User Id=***;Password=***");
    }
}

Ve son olarak yapmamız gereken aşağıdaki migration kodlarını sırasıyla tetikleyerek oluşturduğumuz modelde bir veritabanını sunucuya yansıtmaktır.
dotnet ef migrations add mig_1
dotnet ef database update

Evet, artık temporal table’ları inceleyeceğimiz altyapımız hazır. Şimdi konuya giriş yapabiliriz.

Bir Tabloyu Temporal Table Yapma

EF Core ile bir tabloyu temporal table yapabilmek için aşağıdaki gibi oldukça basit bir tanımlama yapılması gerekmektedir.

class TemporalExampleDb : DbContext
{
    public DbSet<Post> Posts { get; set; }
    .
    .
    .
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .ToTable("Posts", t => t.IsTemporal());
    }
}

Yukarıdaki kod bloğunu incelerseniz eğer override edilmiş olan ‘OnModelCreating’ içerisinde ilgili model üzerinde ‘ToTable’ fonksiyonu eşliğinde ‘IsTemporal’ fonksiyonunu tetikleyerek bu tablonun bir temporal table’ının olacağı bildirilmektedir. Bu değişiklikten sonra yeni bir migration basıldığı taktirde içeriği aşağıdaki gibi olacaktır.
Entity Framework Core - Temporal Tables İle Çalışmak
Yukarıdaki görseli yorumlarsak eğer; sarı ile vurgulanmış kısımda ‘Posts’ tablosu bir temporal table olarak ayarlanmakta ve gerekli açıklamalar yapılmaktadır. Yeşil ve mor kısımlarda ise yapılan verisel değişikliklerin hangi zamanlarda olduğuna dair kayıtları tutacak ‘PeriodStart’ ve ‘PeriodEnd’ kolonları eklenmektedir. Bu kolonların işlevsel mantığını daha net anlayabilmek için makalemizin giriş paragrafında referans ettiğim SQL Server 2016 – Temporal Tables başlıklı makaleye göz atmanızı tavsiye ederim.

Bu vaziyette oluşturulmuş migration’ı migrate edersek eğer ilgili tablo aşağıdaki gibi sunucuya yansıyacaktır.
Entity Framework Core - Temporal Tables İle Çalışmak

Temporal Table’ı Test Edelim

Şimdi, EF Core üzerinden birkaç veri ekleyelim ve ardından temporal table’ın çalışıp çalışmadığını test edelim.

Post p1 = new() { Content = "A Post Content", PostType = PostType.Talk, Title = "A Post Title" };
Post p2 = new() { Content = "B Post Content", PostType = PostType.Talk, Title = "B Post Title" };
Post p3 = new() { Content = "C Post Content", PostType = PostType.Talk, Title = "C Post Title" };

TemporalExampleDb context = new();
await context.AddRangeAsync(p1, p2, p3);
await context.SaveChangesAsync();

Eklenen bu veriler üzerinde rastgele update ve delete operasyonları gerçekleştirelim.

TemporalExampleDb context = new();

Post post = await context.Posts.FindAsync(2);
post.PostType = PostType.Side;
post.Title = "B Post Title - Updated";

await context.SaveChangesAsync();

Yukarıda ‘2’ id değerine sahip veri üzerinde bir update işlemi gerçekleştirilmiştir. Bu işlem neticesinde veritabanında ilgili tablo ile temporal halini sorgularsak eğer aşağıdaki gibi bir sonuçla karşılaşılmaktadır.
Entity Framework Core - Temporal Tables İle ÇalışmakGörüldüğü üzere ‘2’ id değerine sahip verinin önceki kaydı ve hangi zaman aralığında bu değişikliğin yapıldığı temporal table’a kaydedilmiştir. Şimdi ilgili veri üzerinde bir güncelleme işlemi daha gerçekleştirip, inceleyelim.

TemporalExampleDb context = new();

Post post = await context.Posts.FindAsync(2);
post.PostType = PostType.Side;
post.Title = "B Post Title - Updated - 2";

await context.SaveChangesAsync();

Entity Framework Core - Temporal Tables İle Çalışmak

Şimdi ise delete operasyonu çekelim.

TemporalExampleDb context = new();

Post post = await context.Posts.FindAsync(2);
context.Posts.Remove(post);

await context.SaveChangesAsync();

Entity Framework Core - Temporal Tables İle ÇalışmakDikkat ederseniz, ilgili veri silinirken son hali temporal table’a kopyalanır ve ardından kaldırılır.

Evet, anlaşılan EF Core eşliğinde temporal table gayet sağlıklı bir şekilde çalışmaktadır 🙂

Şimdi en önemli noktaya geldik diyebiliriz. Temporal table’ların sorgulanmasına…

EF Core ile Temporal Table’ı Sorgulama

Zamansal değişim verilerini sorgulayabilmek için EF Core çeşitli extension fonksiyonlar sunmaktadır. Bu fonksiyonlar TemporalAsOf, TemporalAll, TemporalFromTo, TemporalBetween ve TemporalContainedIn olmak üzere beş tanedir.

  • TemporalAsOf
    Belirli bir zaman için değişikliğe uğrayan tüm öğeleri döndürür.

    TemporalExampleDb context = new();
    
    var posts = await context.Posts.TemporalAsOf(new DateTime(2022, 01, 23, 08, 49, 45)).Select(data => new
    {
        Post = data,
        PeriodStart = EF.Property<DateTime>(data, "PeriodStart"),
        PeriodEnd = EF.Property<DateTime>(data, "PeriodEnd"),
    }).ToListAsync();
    
    foreach (var post in posts)
    {
        Console.WriteLine($"Id\t\t: {post.Post.Id}");
        Console.WriteLine($"Title\t\t: {post.Post.Title}");
        Console.WriteLine($"Content\t\t: {post.Post.Content}");
        Console.WriteLine($"PeriodStart\t: {post.PeriodStart}");
        Console.WriteLine($"PeriodEnd\t: {post.PeriodEnd}");
        Console.WriteLine("**********");
    }
    

    Entity Framework Core - Temporal Tables İle Çalışmak

  • TemporalAll
    Mevcudiyette bulunan, güncellenmiş yahut silinmiş olan tüm verilerin geçmiş sürümlerini ve geçerli durumlarını döndürür.

    TemporalExampleDb context = new();
    
    var posts = await context.Posts.TemporalAll().Select(data => new
    {
        Post = data,
        PeriodStart = EF.Property<DateTime>(data, "PeriodStart"),
        PeriodEnd = EF.Property<DateTime>(data, "PeriodEnd"),
    }).ToListAsync();
    
    foreach (var post in posts)
    {
        Console.WriteLine($"Id\t\t: {post.Post.Id}");
        Console.WriteLine($"Title\t\t: {post.Post.Title}");
        Console.WriteLine($"Content\t\t: {post.Post.Content}");
        Console.WriteLine($"PeriodStart\t: {post.PeriodStart}");
        Console.WriteLine($"PeriodEnd\t: {post.PeriodEnd}");
        Console.WriteLine("**********");
    }
    

    Entity Framework Core - Temporal Tables İle Çalışmak

  • TemporalFromTo
    Belirli bir zaman aralığı içerisindeki verileri döndürür. Başlangıç ve bitiş zamanı dahil değildir!

    TemporalExampleDb context = new();
    
    var posts = await context.Posts.TemporalFromTo(new DateTime(2022, 01, 23, 14, 55, 59), new DateTime(2022, 01, 23, 15, 00, 04)).Select(data => new
    {
        Post = data,
        PeriodStart = EF.Property<DateTime>(data, "PeriodStart"),
        PeriodEnd = EF.Property<DateTime>(data, "PeriodEnd"),
    }).ToListAsync();
    
    .
    .
    .
    
  • TemporalBetween
    Belirli bir zaman aralığı içerisindeki verileri döndürür. Başlangıç dahil değilken ve bitiş zamanı dahildir!

    TemporalExampleDb context = new();
    
    var posts = await context.Posts.TemporalBetween(new DateTime(2022, 01, 23, 14, 55, 59), new DateTime(2022, 01, 23, 15, 00, 04)).Select(data => new
    {
        Post = data,
        PeriodStart = EF.Property<DateTime>(data, "PeriodStart"),
        PeriodEnd = EF.Property<DateTime>(data, "PeriodEnd"),
    }).ToListAsync();
    .
    .
    .
    
  • TemporalContainedIn
    Belirli bir zaman aralığı içerisindeki verileri döndürür. Başlangıç ve bitiş zamanı dahildir!

    TemporalExampleDb context = new();
    
    var posts = await context.Posts.TemporalContainedIn(new DateTime(2022, 01, 23, 14, 55, 59), new DateTime(2022, 01, 23, 15, 00, 04)).Select(data => new
    {
        Post = data,
        PeriodStart = EF.Property<DateTime>(data, "PeriodStart"),
        PeriodEnd = EF.Property<DateTime>(data, "PeriodEnd"),
    }).ToListAsync();
    .
    .
    .
    

Silinmiş Bir Veriyi Temporal Table’dan Geri Getirme

Silinmiş bir veriyi temporal table’dan bulup geri getirmek istiyorsak öncelikle yapılması gereken ilgili verinin silindiği tarihi bulmamız gerektiğidir. Ardından TemporalAsOf fonksiyonu aracılığıyla silinen verinin geçmiş değeri elde edilebilir ve fiziksel tabloya taşınabilir. Tabi böyle bir durumda, ilgili verinin id sütununa kayıt işleyebilmek için fiziksel tabloya taşıma işleminden önce SET IDENTITY_INSERT komutu çalıştırılmalıdır.

Misal olarak, aşağıdaki örnekte önceden sildiğimiz ‘2’ id’sine sahip olan veri temporal table’dan bulunup geri fiziksel tabloya yüklenmektedir.

TemporalExampleDb context = new();

DateTime dateOfDelete = await context.Posts.TemporalAll()
    .Where(p => p.Id == 2)
    .OrderBy(p => EF.Property<DateTime>(p, "PeriodEnd"))
    // En sonuncu işlem yapılan tarihi alıyoruz.
    // Çünkü bu veri silindiyse muhtemelen en sonuncu
    // işlem yapılan tarihte silinmiştir.
    .Select(data => EF.Property<DateTime>(data, "PeriodEnd"))
    .LastAsync();

//Silinmeden önceki en son kayıt elde ediliyor.
Post deletedPost = await context.Posts.TemporalAsOf(dateOfDelete.AddMilliseconds(-1))
    .SingleAsync(data => data.Id == 2);

//Fiziksel tabloya ekleniyor.
await context.AddAsync(deletedPost);

//Aşağıdaki ExecuteSqlInterpolatedAsync fonksiyonu ile SET IDENTITY_INSERT komutunu tetikleyebilmek için connection'ın açılması gerekmektedir.
await context.Database.OpenConnectionAsync();

//SET IDENTITY_INSERT komutu çalıştırılıyor
await context.Database.ExecuteSqlInterpolatedAsync($"SET IDENTITY_INSERT dbo.Posts ON");
//Sorgu execute ediliyor.
await context.SaveChangesAsync();
//SET IDENTITY_INSERT komutu kapatılıyor.
await context.Database.ExecuteSqlInterpolatedAsync($"SET IDENTITY_INSERT dbo.Posts OFF");

Nihai olarak;
EF Core’un temporal table’ları desteklemesi oldukça hoş ve güçlü bir özellik olmanın yanında, biz geliştiriciler için zamandan tasarruflu bir şekilde verilerimiz üzerinde geçmişe dair analitik ve istatistiksel çalışmalar konusunda büyük ölçüde yardımcı bir niteliktir.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Bunlar da hoşunuza gidebilir...

1 Cevap

  1. erdem dedi ki:

    Merhaba , ekstra herhangi bir konfigürasyon yapmadan direk kullanmaya çalıştım IsTemporal ı , ama periodstart ve periodend deki saat bilgisi 3 saat geriden geliyor. Mssql de ki sistem saatini kontrol ettiğimde saat doğru. Sorun neden kaynaklı olabilir.?

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir