16 Mart 2011 Çarşamba

[Tekerleği Yeniden Keşfet] Katılgan ORM

Merhaba Arkadaşlar;

Bu makalenin kaynak kodlarını CodePlex'deki Adresinden (KatılganORM.rar) indirebilirsiniz.

 

ORM(Object Relational Mapper) kelimesini duyunca "Yeni bir ORM mi?" diye sorduğunuzu duyuyor gibiyim Smile Zaten bu yüzden başlığımız [Tekerleği Yeniden Keşfet]..

"Tekerlek bir kez keşfedilmişken, niye yeniden emek vereyim ki?" diye düşünmeyin. İnsanlık 1969'da Amerika'lı astronotların Aya çıkmasının 40 yıl sonrasından bile; Çin uzaya insan göndermeye çalışıyordu. Burada amaç aynı zorlu süreçten geçip; benzer hataları yapıp, ders alıp tecrübe edinmek... Bizde burada kendi küçücük "Katılgan ORM"mizi yaparken aynı şekilde tecrübe etmeye çalışacağız.

Tabii ki çok ihtiyacımız olmadıkça NHibernate'den ya da Entity Framework'den vazgeçmeyeceğiz.. Ama ihtiyacımız olunca da kullanmaktan kaçınmayacağız (Maalesef bir projemde benim ihtiyacım oldu. Laughing)

Amacımız oldukça basit bir ORM çıkartmaya çalışmak. Özellikleri aşağıdaki gibi olacak.

  1. Select All, Select By Example gibi basit işlevleri olacak.
  2. Update,Insert, Delete işlemleri olacak..
  3. Gelen Relational bir kaydı Nesne olarak dönecek(Sanırım Object kelimesi buradan geliyordu :)
  4. Ek olarak Basit bir BaseService aracılığıyla direk DB'ye sorgu atmamıza izin verecek, istersek bir sınıfa map edecek..

Kodumuzu olabilidiğince küçük tutumak için DB bağımsızlığını tam olarak implemente etmeyeceğiz. Bu da size ödev olsun :)

Her şeyden önce DB ve Entity'mizi görelim.

public class Kisi
{
[PrimaryKey(IsIdentity = true)]
public int KisiID { get; set; }
public string Ad { get; set; }
public string Soyad { get; set; }
public string TelNo { get; set; }
}




Gördüğünüz gibi, entity sınıfımız olabildiğince basit.. PrimaryKey Attribute'u var o da yine çok basit bir sınıf.



İlk adım Program.cs üzerinde yeni bir kişi nesnesi oluşturup DB'ye göndermek, ardından da edindiği ID değerini ekrana yazdırmak.



 



DataContext<Kisi> kisiContext = new DataContext<Kisi>();
Kisi yeniKisi = new Kisi { Ad = "Tahir", Soyad = "çakmak", TelNo = "9876543" };
kisiContext.InsertEntity(yeniKisi);

Console.WriteLine("Eklenen Kisi'nin ID'si: {0}", yeniKisi.KisiID);


 



DataContext<T> sınıfımız ORM'imizin tek anlamlı, kod barındıran sınıfı.. Üzerinde bütün işlemlerimiz için birer anlamlı method barındırıyor. Tip Güvenlilik için Generic olarak tasarladık.



 



public class DataContext<TEntity> where TEntity : class, new()
{
........
public TE RunCommand<TE>(Func<DbCommand, TE> dbExecute, string conStr)
{
DbConnection con = this.CreateConnection();
DbCommand cmd = con.CreateCommand();
try
{
con.Open();
return dbExecute(cmd);
}
catch (Exception e)
{
throw new Exception("DB Hatası", e);
}
finally
{
con.Close();
}
}
........
}


 



DataContext sınıfımızın en temel methodu "RunCommand" methodu; bu bize bütün DB isteklerinin tek bir noktadan akmasını sağlıyor. Dikkat ederseniz, bu method bir "Template Method Pattern"inin C# 3.0 implementasyonu.. İçerisine DBCommand'ı parametre olarak alıp, dışarıya veri dönen tüm methodlar için işlem noktası.. Diğer noktalarda, Con'ın açılıp-kapanması, ya da hata'ların yakalanması ile ilgili merkezi nokta..



 



public bool InsertEntity(TEntity entity)
{
return RunCommand(cmd =>
{
object identityValue = GetInsertQry(entity, cmd).ExecuteScalar();
var propInfo = entity.GetType().GetProperties().Where(pInfo => pInfo.GetCustomAttributes(typeof(PrimaryKeyAttribute), false).Length > 0).FirstOrDefault();
if (propInfo != null && (propInfo.GetValue(entity, null) == null || ((int)propInfo.GetValue(entity, null)) == 0))
{
propInfo.SetValue(entity, Convert.ChangeType(identityValue, Nullable.GetUnderlyingType(propInfo.PropertyType) ?? propInfo.PropertyType), null);
}
return true;
});
}

public DbCommand GetInsertQry(TEntity entity, DbCommand cmd)
{
if (entity is BaseEntity) { (entity as BaseEntity).IsSaving = true; }

string columns = "";
string parameters = "";
foreach (PropertyInfo info in typeof(TEntity).GetProperties().
Where(p => p.GetSetMethod() != null
&& p.GetCustomAttributes(typeof(NonDbColumnAttribute), false).Count() <= 0
&& (p.GetCustomAttributes(typeof(PrimaryKeyAttribute), false) as PrimaryKeyAttribute[]).ToList().Where(pr => pr.IsIdentity).Count() <= 0
)
)
{
object columnValue = info.GetValue(entity, null);
if (columnValue != null)
{
columns += string.Format("{0},", info.Name);
parameters += string.Format(" @{0} ,", info.Name);
cmd.Parameters.Add(this.CreateParameter(info.Name, columnValue));
}
}

StringBuilder querySb = new StringBuilder();
querySb.AppendFormat("INSERT INTO {0} ({1}) VALUES ({2})", typeof(TEntity).Name, columns.Remove(columns.LastIndexOf(","), 1), parameters.Remove(parameters.LastIndexOf(","), 1));

cmd.CommandText = querySb.ToString() + "; SELECT SCOPE_IDENTITY();";

return cmd;
}


 



GetInsertQuery methodu adı üsütünde :) InsertQuery'sini Parametreleriyle beraber oluşturup geriye DBCommand döndürüyor.



Burada yaptığı şey aslında çok basit; reflection ile gelen entity'nin tüm değerlerini alıyor, tek tek kolon ismiyle birebir olarak Insert cümleciğine yerleştiriyor; parametre olarak da değerini veriyor. Property'leri dolaşırken önemli bir nokta var. Metodumuz; PrimaryKey ve Identity olan property'leri, NonDbColumnAttribute attribute'una sahip property'leri, ve de Set özelliği olmayan  property'leri DB'ye göndermiyor..  



Peki gelen kayıtları nasıl nesneye dönüştüreceğiz, reflection ve Convert.ChangeType(...) methodu ile Laughing



 



public List<TEntity> GetAllData(string orderByColumn = "", params string[] columnNames)
{
return RunCommand(cmd =>
{
cmd.CommandText = GetSelectQry(columnNames) + (orderByColumn == "" ? "" : " ORDER BY " + orderByColumn);
return MapEntity(cmd.ExecuteReader());
});
}

public string GetSelectQry(params string[] columnNames)
{
StringBuilder sb = new StringBuilder();
sb.Append("Select ");
if (columnNames.Length > 0)
{
for (int i = 0; i < columnNames.Length; i++)
{
sb.Append(columnNames[i]);
if (i != columnNames.Length - 1) { sb.Append(","); }
}
}
else { sb.Append(" * "); }

sb.AppendFormat(" From {0}", typeof(TEntity).Name);
return sb.ToString();
}


 



Yukardıdaki kısımlar, Insert bölümünden farksız.. Yine string işleme.. Ve sistemin kalbi.. Mapping kod bloğu



 



public List<TEntity> MapEntity(IDataReader dr, params string[] exceptColumns)
{
List<TEntity> entityList = new List<TEntity>();
while (dr.Read())
{
TEntity entity = new TEntity();
var props = typeof(TEntity).GetProperties();
foreach (PropertyInfo info in props)
{
foreach (DataRow row in dr.GetSchemaTable().Rows)
{
if (row[0].ToString().ToLowerInvariant() == info.Name.ToLowerInvariant())
{
info.SetValue(entity, FormatHelper.AssignValue(dr[info.Name], Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType, !exceptColumns.Contains(info.Name)), null);
}
}
}
entityList.Add(entity);
}
return entityList;


Burada bize en çok yol gösteren IDataReader üzerindeki GetSchemaTable() methodu. Veritabanından gelen kolonların tiplerini belirtiyor; böylece gerekliyse bizim dönüşümleri sağlamamıza yardımcı oluyor.. Bir diğer durum da Nullable alanların gerçek tiplerini almamızı sağlayan Nullable.GetUnderlyingType metodu..



Kaynak kodları karıştırıyorken, BaseService<T> sınıfıyla karşılaşacaksınız. Bu sınıf DBContext<T> sınfını kullanan; Data Access Facade amacıyla kullanılan Servis katmanı için bir üst sınıfdır.



Sistem genel olarak Select, Update ve Delete için aynı işlemleri tekrar etmek üzerine gidiyor.. Ancak buraya kadar ki kısmın bir sonraki bölümleri incelemkte yeterli olduğunu düşünüyorum. Ve Sizleri kaynak kodlarla başbaşa bırakıyorum Laughing

Hiç yorum yok:

Yorum Gönder