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

ADO

ADO.NET ile Concurrency Violation  

            Çok kullanıcılı bir program yazacaksınız yani programınız birden fazla kullanıcı tarafından kullanılacak ve merkezi bir veritabanı üzerinde çalışacak. Mesela bir SQL Server sunucusunda bulunan veritabanız üzerinde birden fazla program aynı anda işlem yapabilecek. Bu basit bir konu aslında, SqlClient aduzayı(namespace) altında bulunan sınıf kütüphaneleri ile yapabilirsiniz, ki zaten Sql Server bu iş için biçilmiş kaftandır. Bu yazıda ado.net programlama ile ilgili temel bilgiye sahip olduğunuzu varsayıyorum. Programınız birbirlerinden habersiz kişiler tarafından kullanılacaktır.Yani o anda bir kullanıcı diğer kullanıcıların veritabanı üzerinde hangi işlemleri yaptığını bilemez o kendi işine bakar. Burada verinin doğruluğunu sağlamak biz programcılara düşer.

            Buraya kadar herşey kafanızda tam anlamıyla oturmamış olabilir, örnekler ile anlattıkça daha iyi anlaşılacaktır. Mesela elimizde aşağıdaki gibi bir tablo olsun.

personelno

isim

soyisim

333

Erman

ÖZGÜR

 Saat 13:00, Kullanıcı1 bu satırı okur.
Saat 13:01, Kullanıcı2 de bu satırı okur.
Şimdi iki kullanıcıda aynı veriye sahip.
Saat 13:05, Kullanıcı2 Erman ismini Şaban olarak güncelliyor.
Saat 13:06, Kullanıcı1 ise kendi okuduğu satırda hala Erman olarak gözüken veriyi Ramazan olarak değiştirmek istiyor. Ve burada Concurrency ihlali (violation) oluşuyor. Çünkü Kullanıcı1 verileri son okuduktan sonra Kullanıcı2 veriler üzerinde değişiklik yapmıştır.

Eğer kullancı1 update işlemini Update tabloadi set isim=Ramazan where personelno=333 gibi bir sorgu ile yaparsa herhangi bir hata alması sözkonusu olmaz. Çünkü sorgu doğru satırı güncellemek için personelno alanı ile filtreleme yapıyor. Bu örnekde bu alan değişmediğine göre güncelleme kullanıcı1 in istediği şekilde yapılır. Ama isim alanı kullanıcı1 in son veri çekme işleminden sonra değişmişti. Bu durumda buna bir çözüm bulmak gerekir. Kullanıcının son veri çekme işleminden sonra kayıtda bir değişiklik olup olmadığını tespit etmemiz gerekmektedir. Ve ona göre güncelleme yapmamız gerekir. Bunun için şöyle bir sorgumuz olsaydı.

Update tabloadi set isim=Ramazan where personelno=333 and isim=Erman and soyisim=ÖZGÜR

Kullanıcı1 update işlemini başarısızlık ile sonuçlandıracaktı. Çünkü where sorgusu ile verilen kriterlere uygun bir satır yoktur.Hatırlayın! Kullanıcı2 Erman ismini Şaban olarak değiştirmişti ama bu işlemden sonra Kullanıcı1 verilerini databaseden tekrar çekmemişti.

Concurrency sorunumuza böyle bir çözüm bulduk.Programımız içerisinde ise sql sorgusundan etkilenen kayıt sayısını öğrenir ve eğer sıfır ise kullanıcıya işlemin başarısız olduğunu söyleyip, seçenekler sunabiliriz. Ama bu her update işlemi için uğraştırıcı bir iş olacaktır. Ki zaten .net framework bize bu konuda bir kolaylık sunuyor. SqlDataAdapter(veya OleDbDataAdapter) ve Dataset sınıfları concurrency hata yakalaması konusunda bize çok büyük kolaylık sunmaktadır. 

Not:Concurrency ihlaline karşı iki yöntem ile savaşabiliriz.Birisi database i kilitleyerek Pessimistic Concurrecny, diğeri ise benim burada anlatmaya çalışacağım yöntem Optimistic Concurrency.

Şimdi bu konuda bir örnek yapalım:

Sql server üzerinde aşağıdaki tablo yapısında bir tablo oluşturalım.Bunun için uğraşmak istemiyorsanız. İndirdiğiniz zip dosyası içersinde gerekli olan .sql dosyasını bulacaksınız. Sql Server Query Analyzer kullanarak bu dosya ile tablonuzu oluşturabilirsiniz. Ben tabloyu TestTabani isimli database de oluşturdum. Eğer farklı bir database içerisinde oluşturursanız programın içerisinde connectionstring değişkenini kendinize göre düzenleyiniz.

 

Yukarıdaki tablo yapısını oluşturduktan sonra aşağıdaki şekilde örnek üç adet kayıt girdik. Eğer tablo oluşturmak için zipteki .sql dosyasını kullanırsanız bu kayıtlar tabloya eklenecektir.  

Data erişimiz için bir sınıf oluşturup programımız içersinde tekrarlanan database işlemleri ile uğraşmıyoruz.

vt.vb

Imports System.Data
Imports System.Data.SqlClient
Public Class vt

    Public ds As New DataSet()
    Dim da As New SqlDataAdapter("select * from musteriler", _
    "server=localhost;user id=sa;password=;initial catalog=TestTabani")
    Dim cb As New SqlCommandBuilder(da)
    Public Sub FillDataSet()
       
ds.Clear()
        da.Fill(ds, "musteriler")
    End Sub

    Public Function UpdateDataSet() As Integer
        Return da.Update(ds.Tables("musteriler"))
    End Function

End Class

     Yukarıdaki kodlar veritabanina bağlanıp musteriler tablosunu ds isimli dataset nesnemize yüklüyor ve bu ds dataset nesnesinde bulunan musteriler tablosunu veritabanına yazabiliyor. Bu satırları açıklamıyorum  çünkü baştada dediğim gibi temel ado.net bilginizin olduğunu varsayıyorum. Ama burada SqlCommanBuilder sınıfının bir örneği olan cb nesnesinin işlevinin altını çizmek gerekir. SqlDataAdapter nesnemizin SelectCommand sorgusunu baz alarak SqlDataAdapter nesnemizin InsertCommand,DeleteCommand ve UpdateCommand sorgularını oluşturur.Aşağıdaki komut ile cb nesnemizin bizim için oluşturduğu UpdateCommand Sql sorgusunu alabiliriz.

cb.GetUpdateCommand.CommandText.ToString

cb nin oluşturduğu update sql komutu.Bu komut için gereken parametreler ve yönetimine bizim müdahale etmemize gerek yok.

UPDATE musteriler SET MusNo = @p1 , isim = @p2 , tel = @p3 , adres = @p4 WHERE ( (MusNo = @p5) AND ((isim IS NULL AND @p6 IS NULL) OR (isim = @p7)) AND ((tel IS NULL AND @p8 IS NULL) OR (tel = @p9)) AND ((adres IS NULL AND @p10 IS NULL) OR (adres = @p11)) )

Bu sınıfı veri erişiminde kullanacağız.

Şimdi aşağıdaki resimde gözüktüğü gibi bir form dizayn edin.

Birinci buton btnFill isminde ve text özelliği “Fill”, ikinci buton btnUpdate isminde ve text özelliği “Update” olarak ayarlayın ve datagrid nesnemizin ise ismini Datagrid1 olarak bırakın.

Veri erişimi için oluşturduğumuz sınıfın bir örneğini form sınıfı içerisinde tüm metodlar tarafında kullanılabilmesi için methodların dışında tanımlayarak oluşturun.

Dim x As New vt()

Formun Load eventi tarafında tetiklenecek olan methodu aşağıdaki gibi düzenleyin.

Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load

        x.FillDataSet()
        DataGrid1.DataSource = x.ds.Tables("musteriler")       

End Sub

 btnFill buton nesnesinin Click eventi tarafından tetiklenecek olan methodu aşağıdaki gibi düzenleyin.

Private Sub btnFill_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnFill.Click
        x.FillDataSet()
End Sub

btnUpdate buton nesnesinin Click eventi tarafından tetiklenecek olan methodu aşağıdaki gibi düzenleyin.

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

    Dim affected As Integer
    affected = x.UpdateDataSet()
    MsgBox(affected & " adet kayıt güncellendi!", MsgBoxStyle.Information)

End Sub

    Bu aşamaya kadar yaptığımız işlemler ile veritabanımızdaki kayıtlar görüntüleyebilir ve üzerinde işlem yapabiliyoruz. Şekilde de gördüğünüz gibi Ali AK’ın telefon numarasını değiştirdikten sonra kayıt güncellemesinin başarısı ile ilgili bilgi geliyor.

    Peki yazının başında da belirttiğim gibi bir concurrency hatası oluşturalım ve neler olacağını görelim. Bunun için programımızı iki sefer çalıştırın.Birisini vs.net den f5 e basarak çalıştırabiliz, diğerini ise bin klasorü içerisinden çift tıklayarak. İkiside databaseden verileri okuduktan sonra birincisi(çift tıklayarak açtığınız) ile Ali AK’ telefonunu 1234567 den 4320452 yapın (veya her ne isterseniz) Daha sonra update tuşuna basarak 1 adet kayıt güncellendiğine dair mesaj kutusunu görün. Şu anda veritabanınızda Ali AK’ ın telefonu 4320452 olarak değişti. Ama programınızın çalışan ikinci(vs.net ile açılmış olan) haline baktığınızda son veri çekme işleminden sonra databasede değişiklik olmuş olmasına rağmen hala Ali AK’ın telefonu 1234567 olarak duruyor.Bu offline veri erişiminin bir kuralı biz Fill diyene kadar hala en son değişmiş verileri göremeyiz. Şimdi datagrid üzerinde Ali AK’ın ismini Ali AKKAYA olarak değiştirin ve update tuşuna basın. Ta! Ta!

Not:Vs.net üzerinden ctrl+f5 tuşlarına iki sefer basarak programı iki sefer çalıştırabilirdiniz fakat o zaman aşağıdaki hata kutusu yerine başka bir hata kutusu görürsünüz. Ama sonuçta aynı exception, DBConcurrencyException exceptionı(istisna) alacaksınız.


Not: Concurrency durumuna sebep olmak (test işlemi yapacağımız için)  için programı iki sefer çalıştırmanız şart değildir. Programı bir sefer çalıştırıp verileri çektikten sonra sql server enterprise manager aracılığı ile veri üzerinde değişiklik yaparak hatanın oluşmasına sebep olabilirsiniz.

Şimdi bu hatayı yakalamamız (try-catch) ve daha sonra kullanıcıya gerekli bilgiyi verip seçenek sunmalıyız.

btnUpdate buton nesnesinin Click eventi tarafından tetiklenecek olan methodu aşağıdaki gibi düzenleyiniz.

Private Sub btnUpdate_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnUpdate.Click

  Try

     Dim affected As Integer

     affected = x.UpdateDataSet()

     MsgBox(affected & " adet kayıt güncellendi!",MsgBoxStyle.Information)

  Catch ex As DBConcurrencyException

     Dim sonuc As DialogResult

     sonuc = MsgBox("Update işlemi Başarısız Sonuçlandı!" & vbCrLf & vbCrLf & "Hata Metni : " & ex.Message & vbCrLf & "TAMAM : Kayıtları veritabanından yeniden çekmek İçin" & vbCrLf & "İPTAL : Hata oluşan kayıtları görmek için", MsgBoxStyle.OKCancel, "Güncellem Hatası")

     If sonuc = DialogResult.OK Then

        x.FillDataSet()

     End If

  End Try

End Sub

    Yukarıdaki kod ile eğer oluşur ise DBConcurrencyException exceptionunu yakalıyoruz ve kullanıcıya böyle bir hata olduğunu bildirip veritabanından verileri tekrar çekmek isteyip istemediğini soruyoruz. Profesyonel bir uygulama gerçekleştiriyorsanız. Kullanıcaya bunun dışında bu verileri yazmakda ısrar etme seçeneğini sunabilirsiniz.Bunun için ado.net de hazır bir method yok, bu bize bağlı. Bunu dışında bir hata bildirim formu hazırlar ve bunu hata anında kullanıcıya dialog form olarak gösterirsiniz. Hazırladığınız bu form ile az öncede söylediğim gibi kayıdı hataya rağmen, illaki yazma, log tutma  gibi seçenekleri daha kullanışlı bir arabirim ile sunabilirsiniz.  

    Burada Ali AK kaydının yanındaki satırda hatayı belirten bir ünlem işareti var. Hatanın sebebini yukarıda anlatmıştım. Biz ismi değiştiriyorduk fakat diğer tarafda biz veriyi çekmemiz ve düzenleyip update etmemiz işlemleri arasında kalan sürede başka bir kullanıcı(aslında burada bunu yine biz yaptıkJ) Ali AK isimli müşterinin kaydı üzerinde değişiklik yapmıştı! Telefonunu değiştirmişti! Burada Tamam(bende ingilizce windows olduğu için OK) tuşu databaseden verileri tekrar çekiyor ve bizde böylece biz düzenlemek yaparken değiştirilmiş kayıtları göreceğiz. İptal tuşu ise hiçbir işlem yapmıyor ve böylece hangi satırda hata olduğunu belirten ünlem işaretini görüyoruz.

             Bu gibi bir durumda hatayı yakaladıkdan sonra kullancıya durumu bildirerek ne yapmak istediğine karar vermesini sağlamalıyız. Projenizin durumuna göre farklı alternatif çözümler sunabilirsiniz.Ama ilk akla gelen verileri databaseden tekrar çekme seçeneği veya illaki yaz gibi bir seçenek gibi gözüküyor. Bu arada eğer kullanıcı veriyi tekrar databaseden çekme seçeneğini seçerse sadece hata olan satırları tespit edip o satırlardaki verileri çekmek ise bize hız kazandıracaktır.

             Umarım faydalı bir yazı olmuştur.Bu konudaki kendi çözümlerinizi ve görüşlerinizi duymaktan zevk duyacağım.

  Cengiz HAN
cengiz@cengizhan.com

 Başvuru :
.NET Framework SDK Documentation
Optimistic Concurrency  [Visual Basic]
ms-help://MS.NETFrameworkSDKv1.1/cpguidenf/html/cpconoptimisticconcurrency.htm