top of page

Active Record Pattern Nedir?

Merhaba arkadaşlar,

Bu yazımda, Martin Fowler’ın 2003 yılında yayınlanan Patterns of Enterprise Application Architecture kitabında, ismini verdiği Active Record tasarım kalıbını inceleyeceğiz.


















Değineceğim başlıklar aşağıda belirtilmiştir.

· Data Source Architectural Patterns(Veri Kaynağı Mimarisi Kalıpları) nedir?

· Active Record Pattern nedir?

· Ne zaman kullanılır?

· İmplementasyon

· Avantajları ve Dezavantajları


Data Source Architectural Patterns nedir?

Active Record Pattern’inin de dahil olduğu bir tasarım kalıbı çeşididir. Bu çeşide dahil olan tasarım kalıpları, veri erişimini ve veri tabanı ile ilgili işlemleri kolaylaştırmayı amaçlamaktadır. Çoğu, veri tabanını saran bir obje oluşturarak bu işlemleri yapar.

Aşağıda bu gruba dahil olan tasarım kalıpları ve Martin Fowler tarafından yapılan kısa açıklamaları yer alıyor.

· Table Data Gateway

An object that acts as a Gateway to a database table. One instance handles all the rows in the table.

· Row Data Gateway

An object that acts as a Gateway to a single record in a data source. There is one instance per row.

· Active Record

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

· Data Mapper

A layer of Mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.

Active Record Pattern nedir?

Bu patternde de, diğer orm araçlarından aşina olduğumuz gibi veri tabanındaki her bir satır, kodlama tarafında bir entity’e denk gelir. Diğerlerinden ayrıldığı nokta, bu entity üzerinde aynı zamanda temsil ettiği tabloya ait tüm operasyonların (CRUD) yapılmasını sağlayan fonksiyonların da yer almasıdır.

Bu tür entityl’ere active record denir. Her active record, veri tabanına kaydetme ve yüklemeden ayrıca veriler üzerinde işlem yapan business logic’ten de sorumludur. Active record patter’nin en belirgin özelliği data access logic’i business nesnesinin içine yerleştirmesidir.




Fotoğrafta, Person ismindeki bir tabloya ait CRUD operasyonları, business logic metotları ve aynı zamanda bir satıra karşılık gelen entity’nin bir arada olduğu görülebilir.

Active Record ve Row Data Gateway pattern’leri birlerine çok benzerdir. Aralarındaki temel fark, Row Data Gateway’in yalnızca veri tabanı erişimini içermesi, Aktive Record’un ise hem veri tabanı erişimi hem de business logic içermesidir.

Ne zaman kullanılır?

Active Record bazı yazılımcılar tarafından anti-pattern olarak isimlendiriliyor ancak kullanımının faydalı olduğu bazı durumlar ve projeler de olabilir.

Active Record tabanlı ORM’ler uygulamanızı çok hızlı bir şekilde oluşturmasını sağlar. Bu nedenle ucuz olması gereken ve kritik olmayan projeler için iyi bir çözüm olabilir. Bu tür projelere örnek olarak prototipler veya POC’lar(Proof Of Concept) verilebilir.

Active Record, business logic için çok karmaşık olmayan; okuma, yazma, güncelleme ve silme gibi işlemleri yapmamız için iyi bir seçim olabilir. Ana problem, ancak Active Record veri tabanındaki tabloya tam olarak karşılık gelirse iyi bir şekilde çalışır.

İmplementasyon

Aşağıda bu patterni implement etmiş framework’ ler belirtilmiştir.

· Eloquent (Laravel framework’ün bir parçası) — PHP

· RoR ActiveRecord (Ruby on Rails framework’ün bir parçası) — Ruby

· Django ORM (Django framework’ün bir parçası) — Python


Örnek

Örneğimizde active record’umuz için veri tabanı işlemlerini ve verileri mapleme işini Dapper ile yapacağız. Basit bir şekilde CRUD işlemlerini yapan bir active record base sınıfı ve bir active record oluşturacağız. Bu süreçte yapacağımız işlemler, bize bu patternin avantajları ve dezavantajlarını daha iyi bir şekilde gösterecektir.

MsSql’de ActiveRecordPatternDb adında örnek bir veri tabanı ve şeması ActiveRecord olan Person adında da bir tablo oluşturdum.

Öncelikle birden fazla active record’umuzun olacağını göz önünde bulundurarak, her biri için ayrı ayrı, aynı fonksiyonları yazmak yerine ortak olan fonksiyonlar(Save, Update, Delete, Read gibi) için bir interface yazmak daha mantıklı geldi. Bir de base sınıfta sql komutu üreteceğimiz için tablonun bazı bilgilerini de(SchemaName) bu interface üzerinde tanımlamak işimizi daha da kolaylaştıracaktır.


public interface IActiveRecord<out T> : IActiveRecord where T : class, new()
{
    void Save();
    void Update();
    void Delete();
    IEnumerable<T> Read();
    T FindById();
}

public interface IActiveRecord
{
    string GetSchemaName();
}


Yukarıda her tablo’da ortak olan metotların bulunduğu iki interface yazdım.

Aşağıda BaseActiveRecord adında generic olarak gelen active record’a göre sql komutları üreten bir sınıf oluşturdum. Bu sınıfı, Person active record’umuz kalıtım alacaktır.

Bu sınıfta, active record’tan sadece Schema Name ve tablosunun ismi(yani kendi ismi) dışında kalan tüm bilgiler reflection kütüphanesi kullanılarak alındı.

Aşağıda okuma işlemleri(Read ve FindById) diğerlerinden farklı olarak static bir şekilde tanımlanmış. Kitapta da query işlemlerini yapan metotlar static olarak verilmiş. Bunun sebebi, okuma işlemlerini yapmak için bir active record sınıfı instance’ı oluşturmak zorunda kalmamak içindir.


public class BaseActiveRecord<T> : IBaseActiveRecord<T> where T : class, IActiveRecord, new()
{
    private static string _connectionString = "...";
    
    //Insert işleminden sonra kaydedilen datanın id si ile birlikte dönülmesi için oluşturulan sql komutu
    private string CreateFindQuery(T activeRecord)[...]
    //Verilen active record'un property'lerinin ismini, value'sunu ve veri tipini döner
    private static List<Tuple<string, object, string>> GetColumnsWithValues(T activeRecord)[...]
    //T'nin Id hariç diğer property'lerinin(yani colonlarının) isimlerini getirir
    private string[] GetColumnsExceptPK()[...]
    //Insert komutu oluşturur
    private string CreateInsertCommand(string schemaName)[...]
    //Update komutu oluşturur
    private string CreateUpdateCommand(T activeRecord)[...]
    //Delete komutu oluşturur
    private string CreateDeleteCommand(T activeRecord)[...]
        
    public T Save(T activeRecord)[...]
    public void Update(T activeRecord)[...]
    public void Delete(T activeRecord)[...]
    public static IEnumerable<T> Read()[...]
    public static T FindById(int id)[...]
}

Bu işlemlerin nasıl yapıldığına buradan bakabilirsiniz.



public class Person : BaseActiveRecord<Person>, IActiveRecord<Person>
  {
      public int Id { get; set; }
      public string FirstName { get; set; }
      public string LastName { get; set; }
      public int Age { get; set; }

      public void Save()
      {
          this.Id = base.Save(this).Id;
      }
      public void Update()
      {
          base.Update(this);
      }
      public void Delete()
      {
          base.Delete(this);
      }
      public IEnumerable<Person> Read()
      {
          return BaseActiveRecord<Person>.Read();
      }
      public Person FindById()
      {
          return BaseActiveRecord<Person>.FindById(this.Id);
      }
      public string SchemaName = "ActiveRecord";
      public string GetSchemaName()
      {
          return SchemaName;
      }
  }

Yukarıda BaseActiveRecord’u ve IActiveRecord’u kalıtım alan Person isimli bir active recordumuz var. Görüldüğü gibi direkt kendisini base sınıfa parametre geçerek CRUD işlemlerini yapıyor.

Ben yazmadım ama, normalde business logic’ler de Person classına yazılır.



static void Main(string[] args)
{
    Console.WriteLine("************* Create ***********");
    Person createPerson1 = new Person() { Age = 23, FirstName = "Ali", LastName = "Yıldızöz" };
    Person createPerson2 = new Person() { Age = 23, FirstName = "Ahmet", LastName = "Akdoğan" };
    createPerson1.Save();
    createPerson2.Save();
    Console.WriteLine($"createPerson1 Id: {createPerson1.Id}");
    Console.WriteLine($"createPerson2 Id: {createPerson2.Id}");

    Console.WriteLine("\n\n************* Read ***********");
    List<Person> personList = BaseActiveRecord<Person>.Read().ToList();
    foreach (var person in personList)
    {
        Console.WriteLine($"{person.Id}-{person.FirstName}-{person.LastName}-{person.Age}");
    }

    Console.WriteLine("\n\n************* Update ***********");
    Person updatePerson = new Person() { Id = 5, Age = 22, FirstName = "Test-Update", LastName = "Yıldız" };
    updatePerson.Update();
    Person updated = BaseActiveRecord<Person>.FindById(updatePerson.Id);
    Console.WriteLine($"{updated.Id}-{updated.FirstName}-{updated.LastName}-{updated.Age}");

    Console.WriteLine("\n\n************* Delete ***********");
    Person deletePerson = new Person() { Id = 5 };
    deletePerson.Delete();
    Person deleted = BaseActiveRecord<Person>.FindById(deletePerson.Id);
    Console.WriteLine($"IsDeleted for id={deletePerson.Id}:{deleted == null}");

    Console.WriteLine("\n\n************* Read2 ***********");
    List<Person> personList2 = BaseActiveRecord<Person>.Read().ToList();
    foreach (var person in personList2)
    {
        Console.WriteLine($"{person.Id}-{person.FirstName}-{person.LastName}-{person.Age}");
    }
}

Buradan patter’nin nasıl çalıştığına bakabilirsiniz.


Yukarıdaki kodun çıktısı



Avantajları ve Dezavantajları

Avantajları

· Hızlı uygulama geliştirme için iyi bir seçimdir.

· Active record’ları oluşturmak ve anlamak kolaydır.

· Kullanılıp atılan prototiplerin hızla uygulanmasını kolaylaştırır.

· Bir active record’a bakarak direkt olarak veri tabanı tablosunun veri yapısını anlayabiliriz.

· Çoğu ihitiyaç duyacağımız şeyleri aynı yerde kodladığımız için çok fazla katman oluşturmamıza gerek kalmaz. Bu da hızlı bir şekilde projeyi oluşturmamızı sağlar.

Dezavantajları

· Martin Fowler’a göre eğer business logic’iniz karmaşıksa ve entityleriniz arasında bire çok gibi ilişkiler ve koleksiyonlar oluşturmak istiyorsanız, bunu Active Record ile yapmanız çok kolay değil. Bunun da Data Mapper Pattern’ini kullanmamıza yol açacağını söylüyor.

· Bu pattern de nesne tasarımı ve veri tabanı tasarımı birleştirildiği için projenin ilerlemesi, her iki tarafında birlikte refactor edilmesini çok zorlaştırıyor.

· İki katmanı birleştirdiği için bağımlılığın yüksek olması sonucu Unit Test yazmayı zorlaştırması.

· SOLID prensiplerin S(Single Responsibility Principle) harfinin ihlal edilmesi. Her bir active record hem bir satıra denk gelen bir model hem bir data access objesi hem de business logic’leri üzerinde barındıran bir business objesi olması.

Robert C. Martin’in bu pattern ile ilgili düşünceleri…

The problem I have with Active Record is that it creates confusion about these two very different styles of programming. A database table is a data structure. It has exposed data and no behavior. But an Active Record appears to be an object. It has “hidden” data, and exposed behavior. I put the word “hidden” in quotes because the data is, in fact, not hidden. Almost all ActiveRecord derivatives export the database columns through accessors and mutators. Indeed, the Active Record is meant to be used like a data structure.

On the other hand, many people put business rule methods in their Active Record classes; which makes them appear to be objects. This leads to a dilemma. On which side of the line does the Active Record really fall? Is it an object? Or is it a data structure?

Örneği hazırlarken yaşadığım deneyimler

Eğer base bir sınıf oluşturup, Active Recordların yaptığı ortak işlemleri merkezileştirmek ve tekrar tekrar aynı kodu yazmak istemiyorsanız, en çok zorlanacağınız konular veri tabanı ve tablo ile ilgili, base sınıfın ihtiyaç duyacağı bilgileri ona ulaştırmanız olacaktır. Çoğu durumda ulaştıramayacağınız bilgileri reflection yardımı ile ulaşacaksınız. Bu bilgiler(tablonun şema adı, Primary Key adı, tablonun kendi adı vb) en başta active record’un üzerinde meta data olarak tutulursa işler biraz daha kolaylaşabilir.

Tablonun adı, örnekte direkt sınıfın adı olarak verildi bu sayede bu bilgiyi base sınıfa ulaştırmaya gerek olmadı benim için. Ama bu da active record’un adını tablo adı ile aynı olmasını her zaman zorunlu tutuyor. Zaten active record’un property’lerinin, tablosunun kolonlarına bire bir uyması gerektiğini söylemeye gerek yoktur. Çünkü base sınıfta generic tip ile çalıştığımız için ancak reflection yardımı ile property’leri okunabilir. Bunlardan biri yanlış olursa veri tabanına bağlanma ve tabloyu map etme kısımlarında hata çıkabilir. Zaten örnekte map’leme işlemlerini Dapper ile yaptık, eğer bu işlemler için de bir kütüphane kullanmak istemiyorsanız ayrıca bu işlemleri de kendiniz yapmanız gerekir.

Sonuç olarak active record’un veri tabanı ile çok sıkı bir ilişkisi olduğunu görebiliriz . Bir de ben her tablo’nun CRUD işlemleri için ayrı fonksiyonlar yazmak istemiyorum derseniz, belli bir yere kadar soyutlama yapıp sonrasında reflection kütüphanesi ile baya içli dışlı olmak gerekebilir. Bence bu pattern’i uygularken veri tabanının çok değişmemesi gerektiğini göz önünde bulundurarak en baştan her şeyin belirlenmesi ve Database First bir yaklaşım ile projeyi oluşturmaya başlamak işleri daha kolaylaştırabilir.

Demonun linkini buraya bırakıyorum. Teşekkürler.


Kaynaklar

· Patterns of Enterprise Application Architecture, Martin Fowler


44 görüntüleme0 yorum

Son Yazılar

Hepsini Gör
bottom of page