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

Query Object Design Pattern(Sorgu Nesnesi Tasarım Deseni)

Merhaba,

Günümüzde birçok uygulamada veritabanı işlemleri için yaygın olarak ORM(Object Relational Mapping) yaklaşımı tercih ediliyor olsa da birçok küçük ve orta ölçekli şirket tarafından ve hatta geleneksel olarak radikal değişiklikleri kaldıramayacak kadar köklü framework’lere sahip olan büyük şirketler tarafından da Ado.NET’e dayalı olarak uygulama geliştirildiğine de şahit olabilmekteyiz. Tecrübeyle sabittir ki, Ado.NET ile gerçekleştirilen veritabanı çalışmaları, sürekli değişiklik gösteren proje ihtiyaçları neticesinde kaçınılmaz olarak karmaşık hale gelebilmekte ve bu karmaşıklığı yönetmek ve hatta değişiklikler için programcının dikkat becerisine güvenmek gerekmektedir. Halbuki bu karmaşıklığı mümkün mertebe ORM gibi dinamik bir çözümle yönetebilmek ve oluşturulacak sorguları programcının dikkatinden soyutlayabilmek süreç açısından çözümlerin en idealidir kanaatindeyiz. İşte böyle bir durumda veritabanı sorgularının bir nesne üzerinden temsil edilerek geliştiriciden bağımsız bir şekilde yönetilebilmesini sağlayan Query Object Tasarım Desenini kullanabilir ve ideal çözüme bir adım daha yakınlaşarak çalışmalarımıza devam edebiliriz.

Neden Query Object Design Pattern’ı Kullanmalıyız?

Yukarıda giriş paragrafında bahsedildiği üzere uygulama içerisinde ORM kullanılmıyorsa ve kod içerisinde SQL sorguları oluşturulacaksa Query Object Design Pattern ideal bir çözüm sağlamaktadır. Çünkü, geliştiriciler kullanılacak veritabanına ve o veritabanının sorgulama diline tam hakim olamayabilirler. Yahut veritabanına hakim olsalar bile uygulamanın veritabanı şemasını/mantığını bilmeleri/çözmeleri gerekmektedir. Hangi tabloda hangi verilerin tutulduğu, uygulama konfigürasyonlarının nerede tanımlandığı veya tablolar arası ilişkilerin nasıl olması gerektiği vs. gibi birçok durum geliştiricileri ilgilendirecektir. Oysa bir geliştiricinin işi mümkün mertebe elindeki malzemelerle ortaya iş koyabilmek ve bunu yayına sürebilmektir. Bu kadar bilgiyi öğrenmek için vereceği vakit şirket için oldukça maliyetli bir netice ortaya koyabilir. İşte, Quert Object tasarım deseni bu ve bunlara benzer durumların minimum bilgi gereksinimi ile çözülmesini sağlayacak ve geliştiricinin direkt olarak işe odaklanmasını sağlayacaktır.

Quert Object, bir veritabanı sorgusunu temsil eden nesne olarak anlaşılabilir.

Quert Object tasarım deseni sayesinde nesne olarak geliştirilen sorgular ihtiyaç dahilinde kullanıldıktan sonra tekrar ihtiyaca binaen depolanabilmektedir. Yapının güzel yanlarından biriside burasıdır. Ayrıca bu modellemenin ana yararlarından birisinin uygulamayı veritabanı sorgu dilinden tamamen soyutlamasıdır diyebiliriz. Bunu yazımızın devamında daha net bir şekilde inceliyor olacağız.

Query Object Design Pattern Diyagramını İnceleyelim

Query Object Design Pattern(Sorgu Nesnesi Tasarım Deseni)Yukarıdaki diyagram şemasına göz atarsanız eğer ihtiyaç duyulan SQL sorgusunu ve o sorguda kullanılacak kriterleri temsil eden nesnelerin kullanıldığı bir tasarım görmekteyiz. Modelleme, tasarım açısından bu kadar basittir.

Bu tasarım neticesinde oluşturulan Query Object pattern sayesinde, nesnelerin yorumlanarak SQL çıktısı oluşturan bir model ortaya koyulmaktadır. Eğer ki burada nesne yerine bir veriyi yorumlayarak başka bir çıktı oluşturuyor olsaydık bu Interpreter tasarım deseni olacaktı. Bu benzerlikten dolayı kimi programcılar, Query Object pattern’ın gerçek bir tasarım deseni olmadığı, Interpreter pattern’ın sadece farklı bir modellemesi olduğu yorumunda bulunmaktadırlar. Doğrusu bu yorumda haklılık payları yokta değildir! Çünkü Query Object tasarım deseni sayesinde verilere göre de uygun yorumlamalar yaparak çeşitli SQL çıktıları da sunulabilmektedir.

Quert Object, bir yorumlayıcıdır, yani kendisini bir SQL sorgusuna dönüştürebilen bir nesne yapısıdır.

Martin Fowler

Query Object nesneleri tasarlanırken esnek yapılar kurar ve böylece veritabanı sistemlerinin her özelliklerini kullanılabilir hale getiririz. Böylece efektif kullanım sağlayabilir ve sorgulardaki komplekslik oranını arttırabiliriz. Ayrıca, Query Object’ler ile oluşturulacak SQL çıktılarını, çalışılacak veritabanı sistemine göre farklı şekilde elde edebiliriz. Bu farklılıklar sayesinde birden fazla veritabanı sistemi ile çalışabiliriz.

Özellikle karmaşık sorgulama süreçlerinde bu pattern sayesinde oldukça performanslı bir çalışma sergilenmiş olacaktır. Nihayetinde koda gömülü kompleks bir sorgunun süreçteki ihtiyaçlara istinaden modifiye edilmesi ve bakımı oldukça maliyetli olacaktır. Lakin ihtiyaç doğrultusunda olası değişiklikleri en başta öngörerek bu tasarımın temellerinin atılması büyük avantaj sağlayacaktır.

Ne Zaman Kullanılmaz!

Bir olgunun artısı olduğu kadar eksisi de olabilmektedir. Dolayısıyla Query Object nesnelerinin getirisi kadar götürüsü de mevcuttur. Buradaki götürü ilgili nesnelerin oluşturulma maliyeti, bir başka deyişle zahmetidir. Evet, bir Query Object oluşturmak/tasarlamak oldukça zahmetlidir. Bundan dolayı; eğer ki çalışılan projede herhangi bir ORM teknolojisi kullanılıyorsa bu pattern’ın uygulanması kesinlikle tavsiye edilmemektedir. Ha nerede ve ne zaman kullanılacağını soruyorsanız eğer bu sorunun cevabını yukarıdaki satırlarda verdiğim kanaatindeyim 🙂

Avantajları

  • İhtiyaç duyulan SQL sorgusunu parametreli fonksiyonaliteler eşliğinde barındırır. Böylece gelen parametrelere göre dinamik olarak SQL’i oluşturabilir.
  • Veritabanı sorgu dilini geliştiriciden tamamen soyutlar, bağımsız hale getirir.
  • Birçok veritabanını desteklemektedir.

Örnek Tasarım Modellemesi & Kullanım

Bu pattern’ı net bir şekilde örneklendirebilmek için öncelikle bir developer’ın Ado.NET ile koda gömülü sorgularla çalışırken ne gibi bir atmosferde olduğunu simüle ederek bu yaklaşımın gerekliliğini ortaya koymaya çalışalım. Bunun için aşağıdaki gibi tasarlanarak Ado.NET temelleri atılan bir sınıf üzerinden ihtiyaçların giderildiğini varsayarak başlayalım:

    public class DALContext
    {
        readonly static SqlConnection _connection;
        readonly static object _object = new();
        static DALContext()
        {
            lock (_object)
                _connection = new("Server=localhost, 1433;Database=QueryObjectDesignPatternDB;User ID=SA;Password=***");
        }
        async Task<SqlConnection> GetOpenConnectionAsync()
        {
            if (_connection.State == ConnectionState.Closed)
                await _connection.OpenAsync();

            return _connection;
        }
        public async Task CloseConnectionAsync()
        {
            if (_connection.State == ConnectionState.Open)
                await _connection.CloseAsync();
        }
        public async Task<SqlDataReader> ExecuteQueryAsync(string query)
        {
            using SqlCommand command = new(query, await GetOpenConnectionAsync());
            SqlDataReader dr = await command.ExecuteReaderAsync();
            return dr;
        }
    }

Görüldüğü üzere Ado.NET mimarisi eşliğinde veritabanını sorgulamamızı sağlayacak olan temel işlemlerin icra edildiği ‘DALContext’ isimli bir sınıfımız mevcuttur. Bu sınıfı kullanarak aşağıdaki gibi veritabanındaki ‘Products’ tablosuna sorgular gönderilebildiğini varsayalım.

Query Object Design Pattern(Sorgu Nesnesi Tasarım Deseni)

‘Products’ tablosu…

DALContext context = new();
SqlDataReader dr = await context.ExecuteQueryAsync("SELECT * FROM Products");
while (await dr.ReadAsync())
    Console.WriteLine($"Id : {dr["Id"]}\tName : {dr["Name"]}\tStock : {dr["Stock"]}\tPrice : {dr["Price"]}");
await context.CloseConnectionAsync();

Yukarıdaki kodu derleyip çalıştırdığımızda aşağıdaki gibi veritabanından istenilen verilerin elde edildiğini görüyor olacağız.Query Object Design Pattern(Sorgu Nesnesi Tasarım Deseni)Şimdi bir developer bu ve benzeri bir sorguyu yazarken insani halden ötürü aşağıdaki gibi hata(lar) yapma ihtimali olası mıdır?
SELECT *, FROM Products
SELECT Name, Fiyat FROM Products
SELECT Name, Fiyat FROM Urunler
Tabi ki de olasıdır 🙂 Bu hataların olası olmalarının nedenleri üst paragraflarda da bahsedildiği gibi kâh developer’ın anlık bilinciyle ilgili dikkat dağınıklığı kâh ilgili veritabanı diline hakim olamaması gibi türlü sebepler olabilir. Peki, sorgularımız bu kadar mı? Tabi ki de hayır! Temel düzeyde bir uygulamada bile gelecek verilerin filtrelendiği şartlı sorgular ya da farklı tabloların birleştirildiği join’li sorgular kaçınılmaz olarak kullanılmaktadır. Dolayısıyla bu tarz sorguları developer’ın sorumluluğuna bırakırsak komplekslik arttıkça olası hata oranları da muhtemeldir ki artacaktır.

İşte böyle bir durumda sorguları developer odaklı hatalardan arındırabilmek için oluşturulma sorumluluklarını developerlardan soyutlayarak otomatize hale getirebilmek için Query Object pattern’ını uygulamamız gerekmektedir. Şimdi gelin adım adım bu pattern’ın uygulanmasını inceleyelim.

  • Adım 1
    İlk olarak bir sorgudaki kriterleri/şartları(where) tarif edecek olan sınıfı inşa edelim.

        public class Criteria<TModel>
        {
            string @operator;
            string field;
            object value;
            QueryLogicalOperator queryOperator;
    
            public QueryLogicalOperator QueryOperator => queryOperator;
    
            public Criteria(string @operator, string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                this.@operator = @operator;
                this.field = field;
                this.value = value;
                this.queryOperator = queryOperator;
            }
    
            static string DebugField<TKey>(Expression<Func<TModel, TKey>> method)
            {
                string field = method.Body.ToString();
                field = field.Remove(0, field.IndexOf(".") + 1);
                return field;
            }
    
            public static Criteria<TModel> GreaterThan(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new(">", field, value, queryOperator);
            public static Criteria<TModel> GreaterThan<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = DebugField(method);
                return new(">", field, value, queryOperator);
            }
    
            public static Criteria<TModel> GreaterThanOrEqual(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new(">=", field, value, queryOperator);
            public static Criteria<TModel> GreaterThanOrEqual<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = DebugField(method);
                return new(">=", field, value, queryOperator);
            }
    
            public static Criteria<TModel> LessThan(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new("<", field, value, queryOperator);
            public static Criteria<TModel> LessThan<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = DebugField(method);
                return new("<", field, value, queryOperator);
            }
    
            public static Criteria<TModel> LessThanOrEqual(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new("<=", field, value, queryOperator);
            public static Criteria<TModel> LessThanOrEqual<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = DebugField(method);
                return new("<=", field, value, queryOperator);
            }
    
            public static Criteria<TModel> Equal(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new("=", field, value, queryOperator);
            public static Criteria<TModel> Equal<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = DebugField(method);
                return new("=", field, value, queryOperator);
            }
    
            public static Criteria<TModel> Contains(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new("Like", field, $"%{value}%", queryOperator);
            public static Criteria<TModel> Contains<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = DebugField(method);
                return new("Like", field, $"%{value}%", queryOperator);
            }
    
            public static Criteria<TModel> StartsWith(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new("Like", field, $"{value}%", queryOperator);
            public static Criteria<TModel> StartsWith<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = DebugField(method);
                return new("Like", field, $"{value}%", queryOperator);
            }
    
            public static Criteria<TModel> EndsWith(string field, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
                => new("Like", field, $"%{value}", queryOperator);
            public static Criteria<TModel> EndsWith<TKey>(Expression<Func<TModel, TKey>> method, object value, QueryLogicalOperator queryOperator = QueryLogicalOperator.None)
            {
                string field = method.Body.ToString();
                return new("Like", field, $"%{value}", queryOperator);
            }
    
            public string GenerateSql()
                => $"{field} {@operator} {(value is int or long or float or decimal ? value : $"'{value}'")}";
        }
    

    Yukarıdaki kod bloğunu iyice analiz edersek eğer; 3 ile 6. satır aralığında bir sorguda bulunacak kriterlerin temel parçalarını tutacak field’lar oluşturulmuştur. ‘@operator’, kriterin işlevsel(büyük, küçük vs. gibi) operatörünü ifade ederken, ‘queryOperator’ birden fazla kriter arasındaki ve/veya mantığındaki operatörü ifade eder. ‘field’ sorguda hangi kolona kriter uygulandığını ifade ederken, kriterin değerini de ‘value’ ifade etmektedir.

    8. satırdaki ‘QueryOperator’ property’si ise bir sonraki adımda oluşturacağımız ve bu ‘Criteria<T>’ sınıfını kullanacağımız ‘Query’ sınıfı tarafından sorguya kriterleri hangi mantıkta ekleyeceğimizi öğrenmemizi sağlayacak olan property’dir.

    25 ile 31. satır aralığındaki static olarak tanımlanmış ‘GreaterThan’ isimli metot overload’ları oluşturulacak sorguya ‘büyüktür’ mahiyetinde bir şart eklemektedirler. Overload’lara bakarsanız eğer biri manuel olarak parametrelerden ‘field’ ve ‘value’ değerlerini alırken diğeri ise ‘Func<T>’ aracılığıyla bu işlemi gerçekleştirmektedir. Benzer mantıkla; 3339 satır aralığındaki metotlar ‘büyük eşittir’, 4147 satır aralığındakiler ‘küçüktür’, 4955 ‘küçük eşittir’, 5763 ‘eşittir’ şartlarını uygulamaktadırlar.

    6571, 7379 ve 8187. satır aralığındaki metotlar sırasıyla ‘%..%’, ‘..%’ ve ‘%..’ mantığındaki LIKE şartlarını uygulamaktadırlar.

    18 ile 23. satır aralığındaki ‘DebugField’ isimli generic metot ise tüm ‘Func<T>’ parametreli şart metotlarında lambda ile seçilen property’inin kolon adı olarak kullanılabilmesi için string olarak ayırt edilebilmesi işlevini gerçekleştirmektedir.

    Ve son olarak 8990 satır aralığında bulunan ‘GenerateSql’ metodu ise ilgili kriterin sorguya eklenebilmesi için uygun formatta oluşturulmasından sorumludur.

    Tabi bu kod bloğunda kullanılan ‘QueryLogicalOperator’ enum’ı da aşağıdaki gibi bir içeriğe sahiptir.

        public enum QueryLogicalOperator
        {
            And, Or, None
        }
    
  • Adım 2
    Sırada, artık oluşturulan kriterlerin kullanılacağı esas sorguyu temsil edecek olan ‘Query’ sınıfının oluşturulması vardır.

        public class Query<T>
        {
            string baseQuery = "SELECT {0} FROM {1}";
    
            List<Criteria<T>> criterias;
            public Query(string tableNames) : this("*", tableNames)
            { }
    
            public Query(List<Criteria<T>> criterias, string tableNames) : this("*", tableNames)
                => this.criterias = criterias;
    
            public Query(string columnNames, string tableNames)
            {
                this.criterias = new();
                baseQuery = String.Format(baseQuery, columnNames, tableNames);
            }
    
            public Query(Expression<Func<T, object>> method, string tableNames)
            {
                this.criterias = new();
                NewExpression expression = method.Body as NewExpression;
                List<Expression> arguments = expression.Arguments.ToList();
    
                StringBuilder columns = new();
                for (int i = 0; i < arguments.Count; i++)
                {
                    Expression argument = arguments[i];
                    columns.Append(argument.ToString().Remove(0, argument.ToString().IndexOf(".") + 1));
                    if (i != arguments.Count - 1)
                        columns.Append(", ");
                }
                baseQuery = String.Format(baseQuery, columns, tableNames);
            }
    
            public void Add(Criteria<T> criteria)
                => this.criterias.Add(criteria);
    
            public string GenerateWhereClause()
            {
                StringBuilder whereClause = new();
                whereClause.Append("Where ");
                for (int i = 0; i < criterias.Count; i++)
                {
                    Criteria<T> criteria = criterias[i];
                    whereClause.Append(criteria.GenerateSql());
                    if (i != criterias.Count - 1)
                        whereClause.Append(criteria.QueryOperator switch { QueryLogicalOperator.Or => " OR ", QueryLogicalOperator.And => " AND " });
                }
                return $"{baseQuery} {whereClause.ToString()}";
            }
        }
    

    Bu sınıfı da incelersek eğer; 3. satırda oluşturulacak base query formatını görmekteyiz. Bu query süreçte gelecek olan kolon ve tablo bilgisine göre şekilleniyor olacaktır.

    5. satırda, oluşturulacak query’de kullanılacak olan kriterlerin tutulacağı koleksiyon referansı mevcuttur.

    6 ile 33. satır aralığına bakarsanız eğer constructor metotların overload’ları mevcuttur. Bu overload’ları incelersek sorgulama sürecinde kullanılacak kolon ve tablo isimlerini türlü şekilde almakta ve ‘baseQuery’e basmaktadırlar. Bunların arasından tek dikkat edilmesi gereken 18 ile 33. satır aralığında constructor overload’ıdır. Bu overload sorguda kullanılacak olan kolonları ‘Func<T>’ parametresi ile lambda eşliğinde belirleyebilmemizi sağlamaktadır. Yapısal olarak lambda ile anonymous bir nesne olarak bildirilen property’ler ilgili overload içerisinde kolon adı olarak algılanarak ayıklanmakta ve virgülle yanyana birleştirilip ‘baseQuery’de ilgili alana yazılmaktadırlar.

    35. satırda ise generate edilecek sorguya ihtiyaç doğrultusunda kriterlerin eklenmesini sağlayan ‘Add’ fonksiyonu mevcuttur.

    Ve 38 ile 50. satır aralığında ise oluşturulan ‘baseQuery’ eşliğinde kriterlerin harmanlandığı sorguyu oluşturacak olan ‘GenerateWhereClause’ metodu mevcuttur. Bu metot, kriterlerin ‘GenerateSql’ metodunu tetikleyerek sorguya eklenebilir halde oluşturulmalarını ve ‘baseQuery’ üzerinde ‘Where’ şartı olarak eklenmelerini sağlamakta ve nihai olarak sorguyu bizlere return etmektedir.

  • Adım 3
    Son olarak query’i oluşturma sürecinde ‘Criteria’ ve ‘Query’ sınıflarında ‘Func<T>’ parametreli fonksiyonları kullanabilmek için bir türe ihtiyacımız olacaktır. Bu tür veritabanındaki tabloya karşılık gelecek olan entity modelidir. Çünkü ancak bir entity üzerinden gönül rahatlığıyla tip güvenli sorgulama, kolon yahut şart belirleme işlemlerini tutarlı bir şekilde gerçekleştirebiliriz. Dolayısıyla aşağıdaki gibi ‘Products’ tablosunu modelleyen ‘Product’ sınıfını inşa edelim.

        public class Product
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public int Stock { get; set; }
            public float Price { get; set; }
        }
    
  • Adım 4
    Artık sorgularımızı oluşturabilir ve kriterlerimizi belirleyebiliriz.

    Misal, manuel parametreli fonksiyonları kullanarak ‘Name’ içerisinde ‘i’ harfi geçen ve ‘Stock’ bilgisi 100’den büyük olan ürünleri getirecek olan sorguyu oluşturalım.

    Query<Product> query = new("Products");
    query.Add(Criteria<Product>.Contains("Name", "i", QueryLogicalOperator.And));
    query.Add(Criteria<Product>.GreaterThan("Stock", 100));
    string _query = query.GenerateWhereClause();
    
    Console.WriteLine(_query);
    

    Bu çalışma neticesinde aşağıdaki sorgu üretilecektir.
    SELECT * FROM Products Where Name Like '%i%' AND Stock > 100

    Ya da ‘Price’ı 10’dan büyük eşit olan veya ‘Name’ değeri ‘Çanta’ olan ürünlerin sadece ‘Id’ ve ‘Name’ bilgilerini getirecek sorguyu ‘Func<T>’ eşliğinde oluşturalım.

    Query<Product> query = new(p => new
    {
        p.Id,
        p.Name
    }, "Products");
    query.Add(Criteria<Product>.GreaterThanOrEqual(p => p.Price, 10, QueryLogicalOperator.Or));
    query.Add(Criteria<Product>.Equal(p => p.Name, "Çanta", QueryLogicalOperator.None));
    string _query = query.GenerateWhereClause();
    

    Bu çalışmanın da çıktısı aşağıdaki gibi olacaktır.
    SELECT Id, Name FROM Products Where Price >= 10 OR Name = 'Çanta'

İşte, görüldüğü üzere Query Object pattern her bir veritabanı sorgusunu, temsil eden bir generic nesne olarak tasarlamamızı ve ihtiyaca dönük sorguları tip güvenli bir şekilde fonksiyonaliteler eşliğinde oluşturmamızı sağlamakta ve böylece geliştiricinin sorgu süreçlerinde yükünü minimuma çekip olası maliyetlerden geliştirme sürecini arındırmaktadır.

Tabi ki de bu pattern’ı inşa ederken genel stratejisinin haricinde geri kalan özellik ve fonksiyonları kendi ihtiyaç ve düşüncenize göre tasarlayabilir ve çok farklı niteliklerde işlevsellikler ekleyerek generate edilecek sorgu çeşitliliğini azami miktarda detaylandırabilir ve arttırabilirsiniz.

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

Not : Örnek projeyi aşağıdaki github adresinden edinebilirsiniz.
https://github.com/gncyyldz/QueryObjectDesignPatternExample

Bunlar da hoşunuza gidebilir...

3 Cevaplar

  1. Mehmet dedi ki:

    Efsane bir yazı kendi orm mizi yazabiliriz o zaman

  1. 04 Eylül 2022

    […] hatırlarsanız benzer problemler Query Object Design Pattern‘ında da söz konusuydu. İlgili pattern’da bu problemlere veritabanı sorgusunu temsil […]

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.