Makale Özeti

.Net Framework System.Data namespace i içerisinde yer alan sınıflar ile veri erişimi için olanak sağlar. Bunlar Managed Provider lardır.

Makale

Provider Bağımsız Veri Erişimi

.Net Framework System.Data namespace i içerisinde yer alan sınıflar ile veri erişimi için olanak sağlar. Bunlar Managed Provider lardır. SqlClient namespace i içersinde Sql Server 7.0 ve üzeri versiyonlara veri erişimi için sınıflar sunar. OleDb namespace i ile ise OleDb bağlantıları için gerekli sınıfları sunar. Ayrıca Oracle,SqlCe,ODBC ve MySql (üçüncü parti assembly) içinde managed data providerlar vardır.

Bu durumda projemizde kod yazmaya başlamadan önce hangi veritabanı sistemini kullanacağımıza "kesin" karar vermemiz gerekir. n-tier bir mimari kullanarak veri erişim katmanını ayırabilir ve daha sonraki veri kaynağı değişimlerine sadece veri erişim katmanını tekrar yazarak uyum sağlayabiliriz. Bunun yanında veri erişim katmanımızı ilk defa yazarken daha esnek bir yapı sunması içinde programlayabiliriz. Bu makalenin konusu veri erişim katmanımızı veri kaynağı(database) değişimlerine çok küçük (tek satırlık) değişimler ile uyumlu hale getirmektir.

Veri erişiminde kullandığımız provider sınıfları (xxxConnection,xxxCommand ve diğerleri) .Net Framework içerisinde yer alan bazı interfacelari implemente ederler. Mesela IDbConnection interface i xxxConnection sınıfları tarafından implemente edilir. Aşağıdaki şekilde IDbConnection interface ini implemente eden SqlConnection ve OleDbConnection Sınıfları gözükmektedir.

Şekil-1 System.Data.IDbConnection Interface ini implemente eden SqlConnection ve OleDbConnection sınıfları

.Net Framework dokümantasyonunda System.Data.IDbConnection interface inin overview sayfasına ulaştığınızda aşağıdaki şekli göreceksiniz.


Şekil-2 IDbConnection interface ini implemente eden bağlantı sınıfları.

Veya SqlConnection sınıfının overview bölümüne bakarsanız. Aşağıdaki şekilde gözüktüğü gibi SqlConnection sınıfının IDbConnection interface ini implemente ettiği gözükür.


Şekil-3 SqlConnection Implements IDbConnection

Burada sadece connection sınıfını örneklendirdim. Bu sadece xxxConnection için değil xxxCommand ve diğer veri erişim sınıfları için de geçerlidir. Tabi her biri kendi özelliğine özel interface leri implemente eder. Mesela xxxCommand IDbCommand interface ini implemente eder.

IDbConnection SqlConnection
OleDbConnection
OracleConnection
OdbcConnection
SqlCeConnection
IDbTransaction SqlTransaction
OleDbTransaction
OracleTransaction
OdbcTransaction
SqlCeTransaction
IDbCommand

SqlCommand
OleDbCommand
OracleCommand
OdbcCommand
SqlCeCommand

IDbDataAdapter SqlDataAdapter
OleDbDataAdapter
OracleDataAdapter
OdbcDataAdapter
SqlCeDataAdapter
IDataReader SqlDataReader
OleDbDataReader
OracleDataReader
OdbcDataReader
SqlCeDataReader
IDataParameter SqlParameter
OleDbParameter
OracleParameter
OdbcParameter
SqlCeParameter
IDataParameterCollection SqlParameterCollection
OleDbParameterCollection
OracleParameterCollection
OdbcParameterCollection
SqlCeParameterCollection

Tablo-1 Veri Erişim Sınıfları ve Interfaceler

Yukarıdaki tabloda veri erişiminde kullanabileceğimiz sınıfların ve bunları implemente ettikleri interface lerin listesi verilmiştir.

Buraya kadar her managed provider ın sınıflarının implemente ettiği bazı interfaceler olduğunu anladık sanırım? Bu durumda bizim amacımız olan provider bağımsız veri erişimi için iyi bir giriş noktası bulduk. O zaman bir sınıf tasarlarız ve parametreler ışığında bize istediğimiz nesneleri sunar.

Şimdi yazacağımız sınıfın ayrıntılarına girelim. Örnek ile açıklamak daha anlaşılır olacaktır sanırım. Veri kaynağına erişmek için connection sınıfı gerekli, bunun için bir fonksiyon hazırlayalım. Yani bize connection geri dönen bir method. Ama geri dönüş veri tipini ne yapacağız. SqlConnection veya OleDbConnection veremeyiz çünkü bizden aldığı parametreye ışığında geri dönüş yapan bir sınıf olacak. Yani SqlConnection da dönebilir OleDbConnection da. O zaman geri dönüş tipini IDbConnection yapalım. Nasıl olsa tüm xxxConnection lar IDbConnection ı implemente ediyor.

O zaman hadi bir deneme yapalım. Bizden alacağı integer tipindeki değere göre bize connection nesnemizi dönüyor.

    Public Function CreateConnection(ByVal ProviderType As Integer) As IDbConnection
        Select Case ProviderType
            Case 1
                Return New SqlConnection
            Case 2
                Return New OleDbConnection
        End Select
    End Function

Yukarıdaki kod bloğuna baktığımızda gelen integer 1 ise SqlConnection, 2 ise OleDbConnection döndüğünü görüyoruz. Burada bir hata olmasa da geliştirilmeye açık bir durum var. Yazdığımız bloğu kullanan başka bir programcı veya biz her seferinde hangi rakam neyi ifade ediyordu hatırlamak zorundayız. Bu seçici işlev gören parametreye bir çözüm bulmalıyız. String ile sql veya oledb ifadeler yollayabiliriz. Çalışır mı? -Çalışır. Ama kaliteden yoksun bir yaklaşım olur. Bunun için en iyi çözüm yolu Enumaration kullanmaktır. O zaman seçici işlev görecek Enumaration ımızı tanımlayalım.

    Public Enum enmProviders
        SqlClient
        OleDb
    End Enum
Enumaration kullanmanın bize sağladığı avantajlar vardır. Visual Studio .NET kullanırken IntelliSense özelliğinde faydalanabiliriz. Ve yazarken hata yapmamızı engeller. Hem daha şık durur.:)

Not: Makalenin örnek kodlarında sadece SqlClient ve OleDb namespace sınıfları için örnek verilecektir. Provider sayısını arttırmak istediğiniz de enmProviders enumaration ını genişletip her method a yeni birer "case" ifadesi eklemeniz gerekecektir.

Şimdi fonksiyonumuzu enmProviders tipinde bir parametre alacak şekilde düzenleyelim.

    Public Function CreateConnection(ByVal ProviderType As enmProviders) As IDbConnection
        Select Case ProviderType
            Case enmProviders.SqlClient
                Return New SqlConnection
            Case enmProviders.OleDb
                Return New OleDbConnection
        End Select
    End Function

Gördüğümüz gibi enumaration ile çalışacak şekilde düzenleyelim. Şimdi duruma bakalım: Bir sınıf yazacağız ve yukarıdaki CreateConnection methodu ile benzerlik içeren ve farklı tiplerde geri dönüş yapan methodlar oluşturacağız. Mesela CreateCommand, CreateDataAdapter gibi. O zaman her sınıfa enmProviders tipinde bir parametre belirlemek işi biraz uğraştıcı hale sokacak gibi duruyor. Oluşturduğumuz sınıfa da artık bir isim verelim ben factory isminde bir sınıf oluşturacağım. Ve factory sınıfının constructor ında enmProviders tipinde bir parametre isteyeceğim ve diğer methodlarda parametreye gerek kalmadan bu değişkeni kullanacağım.

Class Factory
    Private _provider As enmProviders
    Sub New(ByVal provider As enmProviders)
        _provider = provider
    End Sub
    Public Function CreateConnection() As IDbConnection
        Select Case _provider
            Case enmProviders.SqlClient
                Return New SqlConnection
            Case enmProviders.OleDb
                Return New OleDbConnection
        End Select
    End Function
End Class

Yukarıdaki sınıf tanımlaması constructorında aldığı enmProviders tipindeki parametreyi private bir değişkende(_provider) saklıyor ve methodlarımız çalışırken seçici değişken olarak bu değişkeni kullanıyor, gayet açık!

Şimdi Tablo-1 deki tabloyu referans alarak diğer methodları da yazalım ve kodumuzun aldığı son şekli görelim.

factory.vb
Imports System.Data
Imports System.Data.OleDb
Imports System.Data.SqlClient

Class Factory
    Private _provider As enmProviders
    Sub New(ByVal provider As enmProviders)
        _provider = provider
    End Sub
    Public Function CreateConnection() As IDbConnection
        Select Case _provider
            Case enmProviders.SqlClient
                Return New SqlConnection
            Case enmProviders.OleDb
                Return New OleDbConnection
        End Select
    End Function
    Public Function CreateCommand() As IDbCommand
        Select Case _provider
            Case enmProviders.SqlClient
                Return New SqlCommand
            Case enmProviders.OleDb
                Return New OleDbCommand
        End Select
    End Function
    Public Function CreateDataAdapter() As IDbDataAdapter
        Select Case _provider
            Case enmProviders.SqlClient
                Return New SqlDataAdapter
            Case enmProviders.OleDb
                Return New OleDbDataAdapter
        End Select
    End Function
End Class
Public Enum enmProviders
    SqlClient
    OleDb
End Enum

Tablo-1 de yer alan her sınıf new ile türetilmez. Bu yüzden factory sınıfımızda her biri için CreateXXX methodu olamaz. Örneğin CreateTransaction diye bir method yapamayız. xxxTransaction nesnesini ancak xxxConnection ın BeginTransaction methodu ile alırız.

Şimdi bu sınıfı nasıl kullanacağımıza bakalım.


Şekil-4 Örnek Uygulama Form Tasarımı

Yukarıdaki şekilde bir form dizayn ettim. Butonların ilki SqlServer a bağlantı kuracak olan bir connectionstring kullanarak, diğeri ise Access veritabanına bağlantı kurmak için gerekli olan connectionstring i kullanarak Sql yazı kutusundaki sql cümlesini çalıştırıyor ve geri dönen sonucu dataset e yükleyip datagrid i dataset e bağlıyor. Buyrun kodlar :

    Private Sub Form1_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
        Me.txtSqlClient.Text = "data source=(local);initial " & _
        "catalog=northwind;trusted_connection=true"
        Me.txtOleDb.Text = "Provider=Microsoft.Jet.OLEDB.4.0;" & _
        "Data Source=C:\My Documents\Northwind.mdb;"
    End Sub

    Private Sub btnSqlClient_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnSqlClient.Click
        LoadData(enmProviders.SqlClient, Me.txtSqlClient.Text)
    End Sub

    Private Sub btnOleDb_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnOleDb.Click
        LoadData(enmProviders.OleDb, Me.txtOleDb.Text)
    End Sub

    Private Sub LoadData(ByVal provider As enmProviders, _
    ByVal strCon As String)
        If Not dgSonuc.DataSource Is Nothing Then
            dgSonuc.DataSource = Nothing
            MsgBox("DataGrid içeriği boşaltıldı")
        End If
        Dim fabrika As New Factory(provider)
        Dim ds As New DataSet
        Dim cn As IDbConnection
        Dim da As IDbDataAdapter
        Dim cmd As IDbCommand
        cn = fabrika.CreateConnection
        da = fabrika.CreateDataAdapter
        cmd = fabrika.CreateCommand
        cn.ConnectionString = strCon
        cmd.Connection = cn
        cmd.CommandType = CommandType.Text
        cmd.CommandText = Me.txtSql.Text
        da.SelectCommand = cmd
        cn.Open()
        da.Fill(ds)
        cn.Close()
        Me.dgSonuc.DataSource = ds.Tables(0)
    End Sub

Form yüklendiği anda gerekli connectionstringleri textbox lara yüklüyoruz. Örnekte büyük çoğunluğunuzun bilgisayarında olan Northwind database ine bağlantı yapıyoruz. Eğer Northwind ın Access versiyonu bilgisayarınızda yoksa sql server dan export edebilirsiniz. (ben öyle yaptım)

Butonların tıklanması ile çalışan methodlarımızdan yardımcı bir method (LoadData) çağrılıyor. LoadData methodunu aynı kodları tekrar yazmamak için oluşturdum. Butonların tıklanması ile yapılacak işlemler zincirinde farklı olması gereken iki değeri parametre olarak aldık. Bunlar enmProviders tipindeki provider ve string tipindeki strCon değişkeni. strCon connectionstring değeridir. provider ise factory sınıfının türetilmesinde parametre olarak verilecek ve factory sınıfı method ları tarafından select-end select ifadesi içerisinde seçici olarak kullanıcaktır.

Şimdi butonlara geri dönelim. btnSqlClient ın tıklanması olayında LoadData methodu enmProviders enumaration ın SqlClient değerini ve txtSqlClient yazı kutusundaki connectionstring değerini parametre olarak veriyor. Şimdi bizim için "esas oğlan" olan LoadData methoduna bakalım. İlk baştaki if kontrolü içerisindeki işlemler dgSonuc ismindeki datagrid kontrolümüze yüklenecek verilerin yüklenmesinde önce boşaltılmasını sağlıyor. If kontrolü ise butonlardan herhangi birine ilk defa tıkladığımız da mesaj kutusunun gelmemesini sağlıyor. Bu kontrolü yapmasaydık takip eden tıklamalarda eğer aynı sql sorgusu ile aynı kayıtları alıyorsak datagrid deki verilerin tekrar yüklendiğini gözlemleyemezdik. Sonraki adımda az önce yazdığımız factory sınıfından yeni bir nesne türetiyoruz(fabrika). Ve bu türetme işleminde factory sınıfının constructorında istediği gibi enmProviders tipinde bir değişken veriyoruz. Bu da LoadData methoduna parametre olarak gelen provider değişkeni. factory den türeyen fabrika nesnemizin Createxxx methodlarını çağırdığımızda verdiğimiz bu değer, dönecek olan nesnenin tipini belirleyecek. Sonraki adımda kayıtları doldurmak için yeni bir dataset nesnesi oluşturuyoruz. Daha sonra fabrika nesnesinin CreateConnection methodundan dönecek olan nesneyi referans etmesi için IDbConnection tipinde bir değişken tanımladık.(türetmedik) Aynı şekilde CreateDataAdapter ve CreateCommand methodlarından dönecek nesneleri referans etmesi için IDbDataAdapter tipinde ve IDbCommand tipinde iki tane değişken tanımladık.(ds,cmd) Sonraki üç adımla ise Createxxx methodlarını çağırıp ilgili değişken tiplerine atadık. Mesela LoadData methoduna provider parametresi olarak SqlClient geldiğini varsayalım, o zaman cn bir SqlConnection nesnesini, da bir SqlDataAdapter nesnesini, cmd ise bir SqlCommand nesnesini referans etmektedir(göstermektedir). Parametre olarak OleDb geldiğinde ise onunla ilgili nesneler türetilecektir. Ve bundan sonra sanki normal yöntemle türetilmiş nesneleri kullanır gibi standart işlemleri yapıyoruz, çünkü artık elimizde ilgili nesneler mevcut. ConnectionString,CommandText,CommandType gibi atamaları ilgili değişkenler ile yaptık. Daha sonra da.Fill ile ds dataset ini doldurup, dgSonuc datagrid inin datasource unu ds datasetinin içindeki ilk tablo (yani yukarıda doldurduğumuz tablo) yaptık. Ve sonuç datagrid de gözüktü.

Factory sınıfında Tablo-1 de verdiğimiz her sınıf için bir method yok çünkü her biri new ile türetilemez. Daha önce de belirttiğimiz gibi örneğin xxxTransaction xxxConnection ın BeginTransaction methodunda bir nesne dönmesi ile türetilebilir.

Factory sınıfında olmayan (new ile direk olarak türetilemediği için) bir nesnenin nasıl provider bağımsız olarak kullanılacağını göreceğimiz bir örnek yapalım. Mesela xxxDataReader türeten ve bu xxxDataReader ile verileri listbox a ekleyen bir uygulama yazalım. Bunun için örnek uygulamaya yeni bir form ekledim ve işlemleri bunun üzerinde yaptım. Forma bir adet buton ve bir adet listbox eklemeniz yeterlidir.


Şekil-5 frmReader Tasarımı

    Private Sub btnLoad_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles btnLoad.Click

        Dim fabrika As New Factory(enmProviders.SqlClient)
        Dim cn As IDbConnection
        Dim cmd As IDbCommand
        Dim rdr As IDataReader
        cn = fabrika.CreateConnection
        cn.ConnectionString = "server=(local);database=northwind;"
        cn.ConnectionString += "integrated security=sspi"
        cmd = cn.CreateCommand
        cmd.CommandText = "select top 5 FIRSTNAME, LASTNAME,"
        cmd.CommandText += "HIREDATE from EMPLOYEES order by"
        cmd.CommandText += " HIREDATE desc, FIRSTNAME asc"
        cn.Open()
        rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection)
        ListBox1.Items.Clear() listbox boşaltıldı
        Do While rdr.Read
            Dim strTemp As String = ""
            strTemp += rdr.GetString(0) & " "
            strTemp += rdr.GetString(1) & " - ("
            strTemp += rdr.GetDateTime(2) & ")"
            Cengiz HAN - (99.99.9999) şeklinde string oluştu
            ListBox1.Items.Add(strTemp)
        Loop
        rdr.Close()

        MsgBox("İşe alınan son 5 eleman listelendi!", MsgBoxStyle.Information)
    End Sub

Şekil-5 de gözüktüğü gibi işe alınan son 5 elemanın ismini ve işe alınma tarihini listeleyeceğiz. Hemen kodu incelemeye başlayalım. Bu örnekte amaç kendi kendine türetilemeyen veri erişim sınıfların (xxxTransaction,xxxDataReader gibi) nasıl provider bağımsız kullanılacağı olduğu için sadece enmProviders.SqlClient parametresi ile türetilen ve dolayısıyla sql server a bağlantı kuran bir örnek yaptım. fabrika nesnesini türetirken verilen parametreyi düzenleyerek ve ilgili connectionstring atayarak oledb namespace i içerisinde yer alan sınıfların kullanılmasını sağlayabileceğinizi zaten önceki örnek ile öğrendiniz. :) Adım adım kodları inceleyelim. factory türünde bir nesne türettik parametre olarak SqlClient parametresini verdik daha sonra Createxxx sınıflarından dönecek nesneleri gösterecek değişkenleri tanımladık. Ve fabrika.CreateConnection ile connection nesnesini (yani bu durumda SqlConnection) cn ye atadık. ConnectionString atamasını yaptık. Ve şimdi önceki örnekten farklı bir satır var. Önceki örnekte xxxCommand türünde bir nesne almak için bizim yazdığımız CreateCommand methodunu kullanmıştık. Burada ise SqlConnection sınıfının kendi methodu olan CreateCommad methodunu kullandık. Bu methodu kullanmak için provider bağımsız uygulama yapıyor olmanız gerekmez. Bu IDbConnection interface ini implemente eden her xxxConnection sınıfında bulunan bir methodtur. Ve xxxConnection sınıfının ait olduğu provider in türünde bir xxxCommand döner. Örneğin SqlConnection dan türeyen bir nesnemizin CreateCommand methodu bize yeni bir SqlCommand nesnesi döner. Ve dönen xxxCommand nesnesinin connection özelliği otomatik olarak ilgili xxxConnection nesnesi olarak atanır. Yani bu ado.net sınıf kütüphanesinin bir özelliğidir. Bu durumda cmd bir SqlCommand nesnesini göstermektedir. Sonraki satırlarda CommandText e sql sorgusu atanmış ve bağlantı açılmıştır. Sonraki satırda ise xxxCommand ın ExecuteReader methodu ile geri dönen xxxDataReader nesnesi, IDataReader tipindeki rdr değişkenine atanıyor. Ve artık rdr bir SqlDataReader nesnesini göstermekte. Buradaki CommandBehavior.CloseConnection parametresi ise bildiğiniz gibi xxxDataReader kapatıldığı zaman üzerinde çalıştığı connection ında kapatılmasını sağlar. Burada bu parametre sadece atraksiyon sağlar. :) Bu parametreyi vermeseydik, sadece rdr.close satırından sonra cn.close satırını eklememiz gerekecekti. Do While-Loop içersinde ise geri dönen kayıtlar listboxda listelenmektedir.

Yukarıdaki örneği xxxParameterCollection, xxxParameter, xxxTransaction gibi nesneler içinde kullanabilirsiniz, sanırım yukarıdaki xxxDataReader örneği bunlar için yeterlidir.

Bitirirken...
Bu makalemizde provider bağımsız veri erişimini ele aldık. Yani yazdığınız bir projenin veri kaynağı (database) değiştiği zaman sadece bir constructor parametresini ve web.config veya app.config de veya bir değişkende sakladığınız connectionstring inizi değiştirmeniz gerekecektir. Makale ve .NET ile ilgili görüş ve önerilerinizi yorum bölümü ile iletebileceğiniz gibi aşağıdaki iletişim yollarıyla da iletebilirsiniz.

Cengiz HAN
email : cengiz@cengizhan.com
web : www.cengizhan.com