Ado.Net 2.0 ile Mars' a Ayak Basıyoruz
İçindekiler
Bu makalemizde, MARS (Multiple Active Results Sets) kavramını incelemeye çalışacağız. MARS kavramı ADO.NET 2.0 mimarisine monte edilmiş yeni bir yapıdır. Bu yapının bize sağladığı avantajları anlayabilmek için, ADO.NET 1.0/1.1 sürümlerinin kabiliyetlerine ve kısıtlamalarına kısaca bir göz atmak gerekmektedir.
Editörün Notu: Final sürümünde, ConnectionString katarı içerisinde MultipleActiveResultSets=true kullanılmaması halinde, MARS etkisi görülmemektedir. Varsayılan olarak MultipleActiveResultSets özelliğinin değeri false’dur. MARS tekniği, MultipleActiveResultSets değeri açıkça true’ya set edildiği takdirde çalışmaktadır.
ADO.NET’in önceki sürümlerinde, özellikle veri kümelerini uygulama ortamına çekmek istediğimizde kullanabileceğimiz iki temel teknik vardır. Birincisi, bağlantısız katmana veri alabilmek için DataAdapter, DataTable, DataSet gibi nesnelerin kullanılmasıdır. İkincisi ise, daha süratli veri çekmemizi sağlayan ve veri kaynağına olan bağlantının SQL komutunun işleyişi boyunca açık olmasını gerektiren, DataReader nesnelerinin kullanıldığı tekniktir.
Bu tekniklerin hangisi kullanılırsa kullanılsın, özellikle aynı bağlantı üzerinden eşzamanlı olarak yürütülen SQL komutlarının çalıştırılmasına izin veren bir yapı yoktur. Yani, veri kaynağından bir DataReader nesnesi ile veri çekerken, aynı açık bağlantı üzerinden güncelleme, ekleme, silme gibi işlemleri içeren SQL komutlarını çalıştıramayız. İşte bu imkânsızlığı gidermek amacıyla ADO.NET mimarisi içine MARS, Asenkron Komut Yürütme ve ObjectSpace gibi yeni ve köklü değişiklikler eklenmiştir. Biz bugünkü makalemizde, aynı açık bağlantı üzerinden birden fazla kayıt kümesini uygulama ortamına çekmemize ve onlara erişmemize imkân sağlayan MARS (Multiple Active Results Sets) yapısını tanımaya çalışacağız.
MARS, özellikle aynı açık bağlantı üzerinden birden fazla satır kümesini elde edebileceğimiz SQL komutlarının eşzamanlı olarak çalıştırılmasına imkân verir. Örneğin ADO.NET’in eski sürümlerinde şu senaryoyu göz önüne alalım; aynı veri kaynağında yer alan ve aynı bağlantı üzerinden erişeceğimiz üç kayıt kümesi olsun. Bu kayıt kümelerini 3 farklı SqlDataReader nesnesi ile ortama çekmek istediğimizi düşünelim. Bu işlemi simüle edebilmek için VS.NET 2003’te basit bir Console uygulaması geliştireceğiz. Uygulamamızın kodları aşağıdaki gibi olacaktır.
using System;
using System.Data.SqlClient;
using System.Data;
namespace MultipleReader
{
class AdoNet1nokta1de
{
[STAThread]
static void Main(string[] args)
{
try
{
SqlConnection con=new SqlConnection("data source=localhost;initial catalog=Northwind;integrated security=SSPI");
SqlDataReader dr1;
SqlDataReader dr2;
SqlDataReader dr3;
SqlCommand cmd1=new SqlCommand("SELECT * FROM Customers",con);
SqlCommand cmd2=new SqlCommand("SELECT * FROM Orders",con);
SqlCommand cmd3=new SqlCommand("SELECT * FROM [Order Details]",con);
con.Open();
dr1=cmd1.ExecuteReader();
dr2=cmd2.ExecuteReader();
dr3=cmd3.ExecuteReader();
con.Close();
}
catch(Exception hata)
{
Console.WriteLine(hata.Message);
}
}
}
}
Uygulamamızı çalıştırdığımızda çalışma zamanında bir istisnanın fırlatıldığını görürüz.

Şekil 1. Ado.Net 1.0./1.1’ deki durum.
Sorun şudur ki, ilk çalıştırılan SqlDataReader nesnesini kapatmadan diğerlerini çalıştırmamız da mümkün değildir. Nitekim SqlDataReader nesnesi, çalıştırdığı SQL komutu ile verileri çekebilmek için ilgili bağlantının kendisi için tahsis edilmesini ve işi bitene kadar da başkaları tarafından kullanılmamasını gerektirir. Bu, tamamıyla ADO.NET’in mimarisinden kaynaklanan bir güçlüktür. Bununla birlikte, akla şöyle bir çözüm yolu gelebilir: Her bir SqlDataReader nesnesini kendi SqlConnection havuzu içinde çalıştırmak. Yani yukarıdaki kodu aşağıdaki gibi yazmak.
SqlConnection con1=new SqlConnection("data source=localhost;initial catalog=Northwind;integrated security=SSPI");
SqlConnection con2=new SqlConnection("data source=localhost;initial catalog=Northwind;integrated security=SSPI");
SqlConnection con3=new SqlConnection("data source=localhost;initial catalog=Northwind;integrated security=SSPI");
SqlDataReader dr1;
SqlDataReader dr2;
SqlDataReader dr3;
SqlCommand cmd1=new SqlCommand("SELECT * FROM Customers",con1);
SqlCommand cmd2=new SqlCommand("SELECT * FROM Orders",con2);
SqlCommand cmd3=new SqlCommand("SELECT * FROM [Order Details]",con3);
con1.Open();
con2.Open();
con3.Open();
dr1=cmd1.ExecuteReader();
dr2=cmd2.ExecuteReader();
dr3=cmd3.ExecuteReader();
con1.Close();
con2.Close();
con3.Close();
Bu hâliyle uygulama çalışacak ve herhangi bir hata mesajı ya da istisna vermeyecektir. Uygulanan bu teknik her ne kadar çözümmüş gibi görünse de, gereksiz yere kaynak tüketimine neden olmaktadır. Nitekim aynı bağlantı için üç kez SqlConnection nesnesi örneklendirilmiş ve SQL sunucusuna üç adet ayrı isimli ama aynı özellikte bağlantı hattı tesis edilmiştir. Dahası bu teknik kullanıldığı takdirde her çalışan SQL komutunun işleyişi sonlanana kadar, bir sonraki SQL komutuna geçilemeyecektir. Bunu çözmek için de, bu veri çekme işlemlerini ayrı metodlar hâlinde tanımlayıp çok kanallı (multi-threading) programlama tekniklerini uygulayabiliriz. Ancak bu yaklaşımlar elbette ki çok verimli ya da ölçeklenebilir değildir.
Çözüm, .NET mimarları tarafından ADO.NET 2.0’a yerleştirilmiştir ve MARS olarak adlandırılmıştır. Şimdi ilk yazdığımız uygulamayı bir de .NET Framework 2.0 ortamında geliştirelim. Kodlarımızı hiç değiştirmeyeceğiz. Bu kez Yukon üzerinde yer alan AdventureWorks isimli veritabanındaki iki tabloya aynı açık bağlantı üzerinden aynı zamanda erişmeye çalışacağız. İşte kodlarımız,
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
using System.Data;
#endregion
namespace MarsEtkisi
{
class Program
{
static void Main(string[] args)
{
try
{
SqlConnection con = new SqlConnection("data source=localhost;initial catalog=AdventureWorks;integrated security=SSPI");
SqlCommand cmd1 = new SqlCommand("SELECT TOP 10 * FROM Person.address", con);
SqlCommand cmd2 = new SqlCommand("SELECT TOP 3 * FROM Person.contact", con);
SqlDataReader dr1;
SqlDataReader dr2;
con.Open();
dr1 = cmd1.ExecuteReader();
dr2 = cmd2.ExecuteReader();
while (dr1.Read())
{
Console.WriteLine(dr1["AddressLine1"]);
}
Console.WriteLine("-------------");
while (dr2.Read())
{
Console.WriteLine(dr2["FirstName"]);
}
con.Close();
Console.WriteLine("-------------");
Console.ReadLine();
}
catch (SqlException hata)
{
Console.WriteLine(hata.Message);
}
}
}
}
Uygulamayı bu haliyle çalıştırdığımızda aşağıdaki ekran görüntüsünü elde ederiz. Görüldüğü gibi, SqlDataReader nesneleri, diğerinin kapatılmasına gerek duymadan veri kaynağından veri çekebilmiştir.

Şekil 2. MARS Etkisi.
Bu örnek basit olarak MARS kavramını açıklamaktadır. Olayı daha iyi kavrayabilmek amacıyla aşağıdaki şekilden de faydalanabiliriz.

Şekil 3. MARS (Multiple Active Results Sets)
Şekilden de görülebileceği gibi, bir veritabanında yer alan çeşitli sayıdaki tabloya ait veri kümelerine aynı uygulama ortamında, farklı ama tek bir ortak bağlantıyı kullanan DataReader nesneleri yardımıyla erişebilmemiz mümkündür.
Şimdi dilerseniz, MARS etkisini daha iyi görebileceğimiz başka bir örneği göz önüne alalım. Bu kez birbiriyle bire-çok ilişkisi olan iki tabloyu ele alacağız. Bu tablolara ait verileri uygulama ortamına yine DataReader nesneleri yardımıyla çekeceğiz. Ancak işin ilginç yanı, bir DataReader nesnesi çalışırken ve satırları arasında iterasyon yaparken, diğerine parametre göndererek her bir satır için diğer DataReader’ın da çalıştırılmasının sağlanmış olacağıdır. Bu örneği gerçekleştirmek için aşağıdaki kodları yazarak, Visual Studio.NET 2005’te basit bir Console uygulaması geliştirelim.
static void Main(string[] args)
{
try
{
SqlConnection con = new SqlConnection("data source=localhost;initial catalog=Dukkan;integrated security=SSPI");
SqlCommand cmd1 = new SqlCommand("SELECT * FROM DepartmanSinif", con);
SqlCommand cmd2 = new SqlCommand("SELECT * FROM PersonelDetay WHERE PersonelID=@PersonelID", con);
cmd2.Parameters.Add("@PersonelID", SqlDbType.Int);
SqlDataReader dr1;
SqlDataReader dr2;
con.Open();
dr1 = cmd1.ExecuteReader();
while (dr1.Read())
{
Console.WriteLine(dr1["Departman"]);
Console.WriteLine(" ALTINDAKI ELEMANLARI....");
cmd2.Parameters["@PersonelID"].Value = dr1["PersonelID"];
dr2 = cmd2.ExecuteReader();
while (dr2.Read())
{
Console.WriteLine(dr2["Ad"]+" "+dr2["Soyad"]+" "+dr2["Mail"]);
}
dr2.Close();
Console.WriteLine("------------");
}
con.Close();
Console.ReadLine();
}
catch (Exception hata)
{
Console.WriteLine(hata.Message);
Console.ReadLine();
}
}
Uygulamamızı çalıştırdığımızda ilişkili tablolara ait verilerin ekrana yazdırıldığını görürüz.

Şekil 4. MARS Etkisi sayesinde içiçe çalışan SqlDataReader’lar.
Örneğimizde, SqlDataReader nesnelerini kullanarak Yukon sunucusu üzerinde yer alan ilişkili iki tabloya ait verileri Console uygulamasından ekrana yazdırmaktayız. dr1 isimli SqlDataReader nesnesi yardımıyla DepartmanSinif isimli master tablodan satırları bir while döngüsü ile okumaktayız. Bu döngünün içerisinde cereyan eden olaylar ise bizim için MARS etkisinin gücünü bir kere daha göstermektedir. Nitekim, dr1 nesnesi ile master tablodan her bir satır okunduğunda bu satıra ait olan PersonelID alanının değeri, başka bir komuta parametre olarak gitmekte ve en önemlisi de bu komutu bir SqlDataReader nesnesi, diğeri hâlen açıkken yürüterek uygulama ortamına sonuç kümesini döndürmektedir. Aynı kod mantığını ADO.NET 1.1 versiyonunda SQL 2000 sunucusu üzerinde gerçekleştirmeye çalıştığımız aşağıdaki örneği göz önüne aldığımızda ise;
try
{
SqlConnection con = new SqlConnection("data source=localhost;initial catalog=Northwind;integrated security=SSPI");
SqlCommand cmd1 = new SqlCommand("SELECT * FROM Orders", con);
SqlCommand cmd2 = new SqlCommand("SELECT * FROM [Order Details] WHERE OrderID=@OrderID", con);
cmd2.Parameters.Add("@OrderID", SqlDbType.Int);
SqlDataReader dr1;
SqlDataReader dr2;
con.Open();
dr1 = cmd1.ExecuteReader();
while (dr1.Read())
{
Console.WriteLine(dr1["OrderDate"]);
Console.WriteLine(" ALTINDAKI ELEMANLARI....");
cmd2.Parameters["@OrderID"].Value = dr1["OrderID"];
dr2 = cmd2.ExecuteReader();
while (dr2.Read())
{
Console.WriteLine(dr2["ProductID"]+" "+dr2["UnitPrice"]+" "+dr2["Quantity"]);
}
dr2.Close();
Console.WriteLine("------------");
}
con.Close();
Console.ReadLine();
}
catch (Exception hata)
{
Console.WriteLine(hata.Message);
Console.ReadLine();
}
Bu kez Ado.Net 1.1’ in mimarisi bu tarz bir işleme izin vermeyecek ve while döngüsü içinden elde edilen ilk tablo satırından sonra, ikinci SqlDataReader nesnesi yürütülmeye çalışıldığında, halen çalışmakta olan SqlDataReader’ın işlemini sonlandırması gerektiğini söyleyecektir.

Şekil 5. Ado.Net 1.1’deki durum.
Görüldüğü gibi, MARS yapısı var olan sınıfların kabiliyetlerini artırarak aynı açık bağlantı üzerinden birden fazla sayıda kayıt kümesine erişebilme imkânına sahip olmamızı sağlamıştır. İşin güzel yanı, MARS etkisini uygulamalarımıza yansıtabilmek için, sınıflarımıza has çeşitli ayarlamalar yapmamıza gerek olmayışıdır. Aynen ADO.NET 1.1 uygulamaları yazar gibi kodlarımızı yazabiliriz. Elbette ki bu mimari değişiklikler sayesinde ADO.NET sınıflarının imkân ve kabiliyetleri önemli derecede artmıştır. ADO.NET 2.0 ile gelen tek yenilik MARS değildir. Bunun yanında, aynı anda birden fazla SQL komutunu çalıştırmamıza yarayan asenkron komut yürütme teknikleri de eklenmiştir. Bu konuyu da bir sonraki makalemizde incelemeye çalışacağız. Hepinize mutlu günler dilerim.