31 Mart 2007 Cumartesi

Generic Oracle DataAccess Layer + Typed DataSet

Data Access Layer için manüel kodlama yapar mısınız? Yapıyorsanız hep aynı kodları yazmaktan sıkılmadınız  mı ? :) (Bulduğunuz çözüm Code Generator yazmak mıdır? Dilin nimetlerinden yararlanın derim.) SQL Server ortamında Data Access Layer için en rahat çözüm tabii ki Typed DataSet yani XSD dosyalarıdır. Typed DataSet'ler üzerinde TypedDataSetGenerator ile çok kolay bir şekilde veri tabanı üzerindeki Store Procedure, Table, View gibi nesnelere ulaşıp gerekli olan kod bloğunu üretebilirsiniz. Nasıl yapılacağını buradan öğrenebilirsiniz. 
SQL Server platformunda Typed DataSet kullanmaya alışmış ve tüm alt yapısını SQL Server üzerine kuran bir yazılımcı Oracle Platformuna geçince ne olur? Bir süre sudan cıkmış balık durumda kalır. :) En büyük veri tabanı üreticilerinden biri olan Oracle'ın .Net platformu için SQL Server kadar hızlı ve kolay bütünleşik bir kod geliştirme aracı ne yazık ki yok. VS içinde ki Server Explorer penceresinden Oracle Packet içeriklerini bile göremiyorsunuz. Odp.Net ile gelen Oracle Explorer penceresi ile ancak Oracle veri tabanınızın detaylarını görebilmektesiniz. Fakat el alışkanlığı ile burada ki nesleri sürükleyip Xsd dosyanıza bırakmak istediğinizde nesnelerin taşınamaz olduğunu görürsünüz. Google'da Oracle Typed DataSet üzerine derin araştırmalar yapıp en sonunda Oracle XSD dosyalarını bir sonra ki VS sürümünde destekleyeceğini açıklayan duyurusu ile tüm hayalleriniz yıkılır. Ama iş beklemez çözüm bulunmalı. Peki, Oracle üzerinde Typed DataSet için 'minimum kodlama ile' Data Access Layer nasıl olacak ki?
Önce DataAccesss Layer nasıl olacak? Typed DataSet kullanan programlarda çalışma mantığı: Veriyi çek, kullanıcıya işlettir ve veri tabanını System.Data.DataAdapter sınıfı ile güncelle şeklindedir. Typed Data Set kullanıyorsanız DataAccess Layer System.Data.DataAdapter nesnesinin bir uygulamasını içermek zorundadır. SQL platformunda bunu TypedDataSetGenerator yaptığı için problem yok. Oracle platformu için çözüm üretelim:

İş ihtiyacımız nedir? Oracle Packetler içinde olan Store Procedure'leri kendi projemiz içinde çağırabilmek ve Typed DataSet üzerinde kullanmak. (Zaten diğer Oracle nesnelerine VS içinden erişilmekte.) Tüm bunları sürekli aynı kodları tekrar etmeden yapmalıyız.

Çözüm: Kod tekrarı yapmamak için Generic Data Access Layer oluşturmak.  İhtiyacımız olan tüm Store Procedure'leri Typed DataTable sınıflarına attribute olarak atamak ve Generic Data Access Layer üzerinde bu attributeleri kullanarak gerekli işlemleri yerine getirmek.(Bir önceki konuda olduğu gibi bu konuda da Attribute sınıflar yardıma koşuyor.) Her zaman ki gibi kodu en üst seviyede yazmalıyız. Sürekli kendisini tekrar eden kodlama yerine en üst seviyede bir defa kod yazmak. Böylece DataAdapter nesnesine olan bağımlılı tek yerden ve kesin bir şekilde ortadan kaldırabiliriz.

Önce Store Procedure'leri tanımlayacağımız Attributeleri yazalım:

 Store Procedure Attribute sınıflarımızı Typed DataTable sınıflarımıza ekleyelim.
[cs]
[SelectProcedure("ACME.PCK_TEST.PRC_GET_ALL_TEST", "refcursor P_TEST:", DefaultSelect = true ) ] 
[SelectProcedure("ACME.PCK_TEST.PRC_GET_TEST_BY_ID","ID1",DefaultSelect=false)] 
[InsertProcedure("ACME.PCK_TEST.PRC_INSERT_TEST", "NAME1", "VALUE1", "out P_ID1 : Int32 id1")] 
[UpdateProcedure("ACME.PCK_TEST.PRC_UPDATE_TEST", "NAME1","VALUE1","ID1")] 
[DeleteProcedure("ACME.PCK_TEST.PRC_DELETE_TEST","ID1")] 
partial class TEST1DataTable 
{ 

}
Neden birden çok Select Procedure var?  DataAdapter nesnesi her bir kayıtın RowState özelliğine göre kendisine ait InsertCommand, UpdateCommand, DeleteCommand fonksiyonlarında biri kullanılarak veriyi güncelleyecektir. Bu sebebten dolayı sadece birer tane Insert, Update ve Delete Store Procedure eklenmesine izin verilmiştir. Birden cok SelectCommand gerekecektir.
Böylece artık Typed DataTable nesneleri Oracle Packet üzerinde ilişkili oldukları Store Procedure'leri bilmektedir. DataTable nesneleri için artık bir Generic DataAdapter nesnesileri oluşturabiliriz. Veri şemasını biliyoruz, veriyi veri tabanında işleyecek Store Procedure'leri biliyoruz o halde Data Access Layer üzerinde sürekli aynı kodları yazmanın gereği var mıdır? En tepeye Generic bir Data Access Layer yazar ve tüm Logic içerisinde bu sınıfı kullanırız. (İp ucu: Yerinizde olsam Base Logic sınıflarınıda Generic yapardım. Bu sayede bütünleşik ve tamamen Generic bir çatınız olur. Projelerinizi minumum kodlama ile bitebilirsiniz.
IDataService ile genel veri tabanı etkileşim ihtiyaçları ortaya konmuştur.

Bu işlemleri gerçekleştiren OracleDataService sınıfıdır ki kendisi projede tek başına Data Access Layer olmaktadır. OracleDataService sınıfı kendisine tip parametresi olarak verilen Typed DataTable nesnesinin StoreProcedure Attribute'lerini almakta ve Odp.Net ile gelen OracleDataAdapter nesnesini bu Attribute'lere göre oluşturmaktadır.
[cs]
StoreProcedureAttribute[] sps = (StoreProcedureAttribute[])table.GetType()
 .GetCustomAttributes(typeof(StoreProcedureAttribute), false); 
foreach (StoreProcedureAttribute att in sps) 
{ 
 if (att is InsertProcedure) 
  InitInsertProcedure(att.ProcedureName, att.Paramters); 
 else if (att is UpdateProcedure) 
  InitUpdateProcedure(att.ProcedureName, att.Paramters); 
 else if (att is DeleteProcedure) 
  InitDeleteProcedure(att.ProcedureName, att.Paramters); 
 else if (att is SelectProcedure) 
  InitSelectProcedure(att.ProcedureName, att.Paramters,
   (att as SelectProcedure).DefaultSelect ); 
 else 
  InitCustomProcedure(att.ProcedureName, att.Paramters); 
}
Odp.Net OracleDataAdapter nesnesine ait Commandları oluştur:
[cs]
private Oracle.DataAccess.Client.OracleCommand 
  InitStoreProcedure(string procedureName, string[] parameters) 
{ 
 Oracle.DataAccess.Client.OracleCommand command = 
  new Oracle.DataAccess.Client.OracleCommand(); 
 command.Connection = _connection; 
 command.CommandType = System.Data.CommandType.StoredProcedure; 
 command.CommandText = procedureName; 
 foreach (string parameter in parameters) 
 {
  command.Parameters.Add(InitParameter(parameter)); 
 } 
 return command; 
}
Kullanımı gayet basittir:
[cs]
Demo.Framework.Generic.Data
 .OracleDataService 
_dataAccessLayer = new Demo.Framework.Generic.Data
 .OracleDataService(); 
public Form1() 
{ 
 InitializeComponent(); 
 _dataAccessLayer.FillAll(dataSet1.TEST1); 
} 

private void tEST1BindingNavigatorSaveItem_Click(object sender, EventArgs e) 
{ 
 _dataAccessLayer.UpdateTable(dataSet1.TEST1); 
}

Değişken parametrele Store Procedure çağrılarını gene OracleDataService sınıfını kullanarak yapabilirsiniz.

Sonuç

Tüm Data Access çağrıları sürekli aynı kodların tekrarı olmaktadır. Genelde manüel kodlama ile veya Code Genetor yazmak ile  bu sıkıcı kod bloğu oluşturulmaktadır. Yukarıda ki yaklaşım ile tekrar edilen kodlardan kurtulmuş olduk. Oracle ve Typed Dataset kullanan uygulamalar için genel (ve tecrübe ile sabit yeterli) bir Data Access Layer yazmış olduk. Aynı yaklaşımı SQL üzerinde de kullanabilirsiniz hatta sadece isim uzayını ve bir iki satır kodu değiştirerek aynı sınıfı kullanabilirsiniz. Fakat ben bunun yerine SQL ortamında size doğrudan XSD dosyalarını kullanmanızı öneririm.

30 Mart 2007 Cuma

Kendi Kendini Doğrulayan Typed DataRow

Verinin kendi kendini test etme yaklaşımı Validation Application Block ile iyice güçlendi. Artık Entity’ler daha zeki ve sakladıkları verinin doğruluğundan Entity nesnesi sorumlu. Validation Application Block veriyi Entity üzerinde saklayan programlar için mükemmel bir yaklaşım sunuyor. Fakat Typed DataRow üzerinde bir Application Block yok. Peki benim gibi üşengeç bir .Net yazılımcısı iseniz Entity nesneleri ile ilişkisel verileri saklamak zor geliyorsa, tüm verileri Typed DataSet içinde taşımak ve işlemek size daha kolay geliyorsa ne yapmalısınız?
Seçenek kalmadı kullandığınız Typed DataSet’leri kendi kendini test edebilir şekle getirmeliyiz. Ne gerekli bize:
  1. Atomik test işlemlerini yapan Attribute sınıflar.
  2. Bu test işlemlerini işletecek Controller sınıfı
Tabi tüm bu sınıfların veriden bağımsız olması yani Generic olması gerekmektedir. Burada ki kısıtlama Validation Application Block her bir Entity Field’a attribute vererek doğrulama yapılmakta fakat Typed DataRow üzerinden ki Field’lara Attribute kalıcı olarak eklenemez. Onun için bizim CheckAtiibute’lerimiz direk Typed DataRow nesnelerine eklenmelidir.
Doğrulama iki şekilde olur:
  1. Check: Veri kendi başına doğru mu sorusudur. Mesela email alanı gecerli mi? Para alanına negatif değer girildi mi?
  2. Validation: Veri iş kurallarına uygun mu sorusudur. Yani veri diğer veriler ile birlikte doğru mu?

Tüm işi gerçekleştiren Controller sınıfıdır. Check hatalarını girildiği anda (OnFly) yakalamak için DataSource almaktadır. DataSource üzerinden veri değiştiği zaman BaseCheck sınıfından türetilen Check kuralları çalıştırılmakta ve karşılaşılan hatalar ColumnError olarak atanmaktadır.

Önce takip edilecek Row’a Attributeler atanmalı: 
[cs]
[NotNullCheck("Column1",UserFriendlyName="Birinci alan")]
[MinimumCheck("Column2",12, UserFriendlyMessage = "İkinci alandaki veri 12 den küçük olamaz")] 
[UniqueCheck("Column3", UserFriendlyMessage = "İkinci alandaki veri tüm tabloda unique olmalı")]
partial class DataTable1Row 
{   

}
Veri değişimi takibi:
[cs]
#region FollowTable 
protected virtual void FollowDataSource(DataTable table) 
{ 
 table.ColumnChanging += 
  new DataColumnChangeEventHandler(table_ColumnChanging); 
} 

protected virtual void table_ColumnChanging(object sender, DataColumnChangeEventArgs e)
{ 
 // veri değişti check kurallarını işlet 
  if( e.Row.RowState != DataRowState.Deleted) 
  e.Row.SetColumnError(e.Column, 
   CheckRow(e.Column, e.Row, e.ProposedValue)); 
} 
#endregion
Değişen veriyi test et:
[cs]
#region check 
///  
/// kolon verisi değişti kontrolü 
///  
/// değişen kolon 
/// kullanılan row 
/// yeni değer 
/// hata iceriyorsa hata mesajı. hata yoksa boş string 
protected virtual string CheckRow(DataColumn dataColumn, DataRow dataRow, object columnNewValue) 
{ 
 dataRow.EndEdit(); 
 string result = string.Empty; 
 BaseCheck[] attrs = (BaseCheck[])dataRow.GetType()
  .GetCustomAttributes(typeof(BaseCheck), true); 
 foreach (BaseCheck attr in attrs) 
 {  
  if (attr.ColumnName == dataColumn.ColumnName) 
  { 
   result += attr.IsValid(dataRow, columnNewValue); 
  } 
 } 
 return result; 
} 
#endregion 
Check işleminin çalışma zamanı görüntüsü:

IsValid çağrısı ile önce Check kontrolleri çalıştırılır eğer tüm checkler doğru ise kontrol edilen Row HasSelfValidation attribute’ne sahip mi kontrolü yapılır. Eğer Row HasSelfValidation attribute’ne sahip ise SelfValidation attribute’ne sahip fonksiyonlar aranır ve bu fonksiyonlar çağırılır.
[cs]
#region validate 
///  
/// Veri tabanına kayıt etmeden önce kayıtın doğruluğunu kontrol et 
/// hata bulunur ise hatayı throw et 
///  
/// kontrol edilecek kayıt 
public virtual bool IsValid(DataRow row) 
{ 
 row.EndEdit(); 
 _errorMessage = string.Empty; 
 if (row.RowState != DataRowState.Deleted 
  && row.RowState != DataRowState.Detached) 
 { 
  foreach (DataColumn col in row.Table.Columns) 
   _errorMessage += CheckRow(col, row, row[col]);                       
   //tüm childe row'ları valid mi 
  foreach (DataRelation relation in row.Table.ChildRelations) 
   foreach (DataRow childeRow in row.GetChildRows(relation)) 
    foreach (DataColumn col in childeRow.Table.Columns) 
     _errorMessage += 
      CheckRow(col, childeRow, childeRow[col]); 
 } 
 // validation 
 if (_errorMessage.Length == 0 && row.RowState != DataRowState.Deleted 
  && row.RowState != DataRowState.Detached) 
 { 
  HasSelfValidation[] attrs = (HasSelfValidation[])row.GetType()
   .GetCustomAttributes(typeof(HasSelfValidation), false); 
  if (attrs != null && attrs.Length > 0) 
  { 
   foreach (MethodInfo method in row.GetType().GetMethods()) 
   { 
    SelfValidation[] validation = (SelfValidation[])method
     .GetCustomAttributes(typeof(SelfValidation), false); 
    if (validation != null && validation.Length > 0) 
    { 
     object result = method.Invoke(row, new object[] { }); 
     if (!string.IsNullOrEmpty((string)result)) 
     { 
      this._errorMessage += (string)result; 
     } 
    } 
   } 
  } 
 } 
 return string.IsNullOrEmpty(_errorMessage); 
} 
#endregion 
Validation kontrollerinin çalışma zamanı görüntüsü: