C# İle Table Data Gateway Design Pattern(Table Data Gateway Tasarım Deseni)
Merhaba,
Bu içeriğimizde herhangi bir ORM kullanmayı tercih etmediğimiz projelerimizde veritabanı ile etkileşime girecek olan operasyonlarımızın sorumluluklarını üstlenecek olan katmanla iş katmanının iletişimini modellememizi sağlayan Veri Katmanı Modelleme Tasarım Desenleri‘nden olan Table Data Gateway Design Pattern‘ı hem teoride hem de pratikte inceliyor olacağız.
Table Data Gateway Design Pattern Nedir?
ORM kullanılmayan projelerde geliştirilen uygulamaların veritabanı işlemleri kod içerisinde belirli algoritmik kontrollerle generate edilmiş olan SQL cümlecikleriyle giderilmektedir. Ee haliyle bu tarz durumlarda veritabanı işlemlerini yürütecek operasyonların uygulama mantığının işlendiği kodlarla karıştırılması bir takım problemlere sebebiyet verebilmektedir. Bu problemlerin en temel kaynağı, geliştiricilerin SQL’e(yahut kullanılan veritabanı diline) pek vakıf olmama durumudur diyebiliriz. Ya da geliştirici ilgili veritabanının diline vakıfsa eğer bu seferde diyagramına tam hakim olamayabilmektedir. Hal böyleyken geliştiricilerin business logic’de çalışırken veritabanına olan ihtiyaçlarını bodoslama koda yerleştirmeleri bahsi geçen bu eksikliklerin süreçte farklı problemler olarak karşımıza çıkma ihtimallerini arttıracağı kaçınılmazdır. Halbuki business logic ile veritabanı operasyonlarının bir şekilde birbirlerinden soyutlanması ve veritabanı işlemlerinin işin uzmanları tarafından geliştirilip, sadece kullanımını developer’lara bırakması oldukça ideal bir yaklaşım olacaktır. Öyle değil mi?
Hani hatırlarsanız benzer problemler Query Object Design Pattern‘ında da söz konusuydu. İlgili pattern’da bu problemlere veritabanı sorgusunu temsil edecek olan Query Object nesneleri oluşturularak çözüm getirilmekteyken, bu pattern’da ise kod içerisinde SQL ile ilgili yapılacak tüm(sadece sorguyu değil, tüm işlemleri) operasyonları basitlik ilkesi ışığında değerlendirip business logic mantığından yalıtarak tek bir nesneyle temsil edip çözüm getirilmektedir. Böylece her iki pattern’da da veritabanı sorgu dilini uygulamadan tamamen soyutlamış bir netice elde edilmektedir.
Query Object pattern, kendisini bir SQL sorgusuna dönüştürebilen bir nesne yapısıyken; Table Data Gateway pattern ise veritabanındaki bir tabloya karşılık tüm operasyonları üstlenen bir sınıftır.
Amacı Nedir ve Nasıl Tasarlanır?
Table Data Gateway deseninin temel amacı uygulamada SQL sorgularının bir standart eşliğinde tek bir merkezi yerde toplanmasını sağlamaktır. Bunun için veritabanındaki her bir tabloya karşılık(bu view ya da stored procedure olabilir) ayrı bir sınıf oluşturarak içerisinde select, insert, update ve delete operasyonlarını barındırır. Bu sınıfların teknik adı ‘Table Data Gateway‘dir. Burada her bir tabloya karşı bir sınıfın inşası her ne kadar zaruri olsa da bazen basit senaryolarda tüm tablolar için tüm işlevsellikleri barındıran tek bir sınıf tanımlayıp, kullanılabilmektedir.
Bir veritabanı tablosuna kod kısmından gateway(geçit) görevi gören ve tüm select, insert, update ve delete sorumluluklarını üstlenen sınıfa Table Data Gateway denir.
Bir Table Data Gateway instance’ı, karşılığı olan veritabanı tablosundaki tüm satırları handler edebilir. Bu süreçte sorgu için gerekli olan parametreleri ilkel olarak metot parametresi şeklinde alabilir ve domain nesnesi(entity) hakkında hiçbir şey bilmeksizin operasyonları gerçekleştirebilir. Çünkü her bir domain nesnesine karşılık gateway sınıfı olmasını gerektirir. Bu duruma örnek olması hasebiyle; ‘Person’ adında bir domain class’ımızın olduğunu düşünürsek eğer buna karşılık veritabanı işlemleri için ‘PersonGateway’ sınıfının olması gerekmektedir. Ayrıca Table Data Gateway sınıfları sadece tek bir tabloya özel olacakları için birbirleriyle herhangi bir etkileşimleri olmayacaktır. Bu yüzden bu pattern’da yeniden kullanılabilirlik(reusable) yoktur.
Ne Zaman Tercih Edilmelidir?
Özellikle karmaşık iş kurallarının olmadığı durumlarda tercih edilebilir. Çünkü sade çalışmalarda gerçekleştirilecek veritabanı işlemleri Table Data Gateway sınıfları aracılığıyla rahatlıkla yönetilebilir ve ihtiyaca binaen kolayca refactoring edilebilir.
Ayrıca veritabanı operasyonlarının tablo bazında gruplanması gerektiği durumlarda da tercih edilebilir. Tabi burada unutulmaması gereken husus şudur ki, Table Data Gateway deseni uygulamayla ilişkili tüm tablolarda aynı mantıkta kullanılmalıdır. Aksi taktirde belirli bir eşiğin üstündeki tablolara sahip olan çalışmalarda refactoring işleminin yapılabilmesi oldukça maliyetli olacaktır.
Table Data Gateway deseni, bir nevi uygulama mantığını veritabanı erişimiyle encapsule etmeye yarar.
Örneklendirelim
Şimdi Table Data Gateway desenini yukarıdaki satırlarda bahsedildiği üzere aşağıdaki ‘Persons’ tablosuna uygun bir şekilde örnek senaryo amaçlı uygulayalım.
İlk olarak ‘SqlConnection’ ve ‘SqlCommand’ nesnelerini bizim için organize eden temel bir sınıf oluşturalım.
public static class Database { static SqlConnection _connection; static Database() { object _lock = new(); lock (_lock) _connection = new("Server=localhost, 1433;Database=TableDataGatewayDB;User Id=sa;Password=1q2w3e4r+!"); _connection.Open(); } public static async Task<int> ExecuteNonQueryAsync(string query, params SqlParameter[] parameters) { SqlCommand command = new(query, _connection); command.Parameters.AddRange(parameters); return await command.ExecuteNonQueryAsync(); } public static async Task<SqlDataReader> ExecuteReaderAsync(string query, params SqlParameter[] parameters) { SqlCommand command = new(query, _connection); command.Parameters.AddRange(parameters); return await command.ExecuteReaderAsync(); } }
Görüldüğü üzere yukarıdaki ‘Database’ isimli static sınıf temel veritabanı erişim sorumluluğunu üstlenmektedir. Dikkatli incelerseniz eğer içerisindeki ‘ExecuteNonQueryAsync’ ve ‘ExecuteReaderAsync’ metotları aracılığıyla sorgu zeminlerini oluşturmakta ve ihtiyaç olabilecek parametreleri almaktadır. Ardından ‘Persons’ tablosu için Table Data Gateway sorumluluğunu üstlenecek olan ‘PersonGateway’ sınıfını oluşturalım.
public class PersonGateway { // Person ekleme public async Task<int> AddPersonAsync(string firstName, string lastName, string country) => await Database.ExecuteNonQueryAsync("INSERT Persons(FirstName, LastName, Country) VALUES(@firstName, @lastName, @country);" , new SqlParameter("firstName", firstName) , new SqlParameter("lastName", lastName) , new SqlParameter("country", country)); //Personel silme public async Task<int> RemovePersonAsync(int id) => await Database.ExecuteNonQueryAsync("DELETE FROM Persons WHERE Id = @id" , new SqlParameter("id", id)); //Personel güncelleme public async Task<int> UpdatePersonAsync(int id, string firstName, string lastName, string country) => await Database.ExecuteNonQueryAsync("UPDATE Persons Set FirstName = @firstName, LastName = @lastName, Country = @country WHERE Id = @id" , new SqlParameter("id", id) , new SqlParameter("firstName", firstName) , new SqlParameter("lastName", lastName) , new SqlParameter("country", country)); //İsme göre personel sorgulama public async Task<List<PersonDTO>> GetPersonByFirstNameAsync(string firstName) { SqlDataReader dataReader = await Database.ExecuteReaderAsync("SELECT * FROM Persons WHERE FirstName = @firstName" , new SqlParameter("firstName", firstName)); List<PersonDTO> datas = new(); while (await dataReader.ReadAsync()) datas.Add(new() { Id = int.Parse(dataReader["Id"].ToString()), Country = dataReader["Country"].ToString(), FirstName = dataReader["FirstName"].ToString(), LastName = dataReader["LastName"].ToString() }); return datas; } //Id'ye göre personel sorgulama public async Task<PersonDTO> GetPersonByIdAsync(int id) { SqlDataReader dataReader = await Database.ExecuteReaderAsync("SELECT * FROM Persons WHERE Id = @id" , new SqlParameter("id", id)); PersonDTO data = new(); await dataReader.ReadAsync(); data.Id = int.Parse(dataReader["Id"].ToString()); data.Country = dataReader["Country"].ToString(); data.FirstName = dataReader["FirstName"].ToString(); data.LastName = dataReader["LastName"].ToString(); return data; } //Tüm personeller public async Task<List<PersonDTO>> GetPersonsAsync() { SqlDataReader dataReader = await Database.ExecuteReaderAsync("SELECT * FROM Persons"); List<PersonDTO> datas = new(); while (await dataReader.ReadAsync()) datas.Add(new() { Id = int.Parse(dataReader["Id"].ToString()), Country = dataReader["Country"].ToString(), FirstName = dataReader["FirstName"].ToString(), LastName = dataReader["LastName"].ToString() }); return datas; } }
Yukarıda görüldüğü üzere ‘PersonGateway’ sınıfını mümkün mertebe tüm işlemleri örneklendirecek şekilde doldurmuş bulunmaktayım. Tabi burada sorgulama neticesinde gelecek olan verileri aşağıdaki ‘PersonDTO’ isimli sınıf eşliğinde modellemekteyim.
public class PersonDTO { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Country { get; set; } }
Velhasıl artık Table Data Gateway deseni sayesinde uygulamamızdaki ‘Persons’ tablosuyla olabilecek tüm operasyonları ‘PersonGateway’ sınıfı eşliğinde tek bir çatı altında aşağıdaki gibi yürütebilmekteyiz.
PersonGateway personGateway = new(); await personGateway.AddPersonAsync("Gençay", "Yıldız", "Ankara"); await personGateway.AddPersonAsync("Nevin", "Yıldız", "Ankara"); await personGateway.AddPersonAsync("Gülşah", "Yıldız", "Ankara"); await personGateway.AddPersonAsync("Emine", "Yıldız", "Ankara"); await personGateway.AddPersonAsync("Elif", "Yıldız", "Ankara"); await personGateway.AddPersonAsync("Muhammet Kürşad", "Yıldız", "Ankara"); var persons = await personGateway.GetPersonsAsync(); foreach (var person in persons) Console.WriteLine($"{person.FirstName} {person.LastName} - {person.Country}");
Nihai olarak,
SQL operasyonlarının manuel olarak yürütüldüğü uygulamalar üzerinde bu işlemlerin bir yerde fiziksel olarak toplanıp merkeziyet kazandırılmasını hedefleyen Table Data Gateway pattern’ı uygulama zorluğu açısından oldukça yalın ve kullanım açısından ise basit yahut orta ölçekli uygulamalar için oldukça elverişlidir. Tabi bu pattern’ın teknik yapısından ziyade projedeki davranışların işlevselliğine göre merkezileştirilmesi gerekliliği prensibinin kavranılması esas önemi taşımaktadır kanaatindeyim.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Not : Örnek uygulamanın kaynak kodlarına aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/Table_Data_Gateway_Design_Pattern
yine efsanevi bir yazı teşekkürler
Teşekkürler