2 Mayıs 2012 Çarşamba

MS SQL 'de Transaction

Merhaba arkadaşlar.
Günlük hayatta, birbirleriyle ilişkili ve ark arkaya çalışması gereken bazı işlemlerimiz vardır. Bu işlemlerimizin hepsinin tek bir işlemmiş gibi birlikte gerçekleşmesi istenir. İşte arkadaşlar MS Sql Server 'da da bu yöntem için "Transaction" denilen bir yapı kullanıyoruz. "Transaction" kelime anlamı olarak, daha küçük parçalara ayrılamayan işlem yığınlarına denir. Yani arkadaşlar "Ya hep ya hiç" mantığı vardır. Yani ya bu işlem topluluğunun hepsi gerçekleşecek, yada hiç biri gerçekleşmeyecek demektir. Bir kısmının gerçekleşip, bir kısmında hata çıkması ihtamali sonucunda, gerçekleşen  işlemlerin de geri alınması istenir. İşte "transaction" 'ın çalışma prensibi bu şekildedir.

NOT: Göreceğimiz örneklerde "transaction" işlemini elle başlatacağız. Bu tip "transaction" 'lara "explicitly transaction" denir. Bir de MS Sql Server 'da "implicitly transaction" olarak "insert, update, delete, triggers vs.." gibi bir çok komut ve nesne, daha güvenilir ve kesin sonuçlar veren işlemler gerçekleştirmesi açısından, alt yapısında "transaction" yapısıylayla çalışır.

Peki ne gibi işlemlerde kullanılır? Örneğin veri tabanından silinen kayıtları, başka bir veri tabanına yedekliyorsunuz. Bir silinme ve bir kaydetme işlemi söz konusu. Bu işlemlerin sırayla ard arda gerçekleşmesi gerekiyor. Silinme işlemi başarılı bir şekilde tamamlandı. Fakat kaydetme işleminde bir hata meydana geldi. Bu hata yetersiz hafızadan olabilir. Bırakın yetersiz hafızayı elektrikler kesilmiş bile olabilir. Her olasılığı düşünmek bizim işimiz, yazılımcı olarak her şey bizim sorumluluğumuz altındadır. Bu yüzden silinmiş kaydıma elveda demektense, bu sorunla karşılaşma ihtimalimize karşı önlemimi almam gerekir. Çalışmasını istediğim kod bloğumu "transaction" bloklarına alıyorum ve sorun çözülüyor.

"Transaction" denilince en sık karşılaşılan örnek "bankada hesaplar arası havale işlemi" örneğidir. Bu örnek hem "transaction" 'ın yapısını kavramak, hem de neden kullanmamız gerektiğinin ciddiyetini anlamak açısından güzel bir örnektir. Bilindiği üzere havale işlemi iki aşamadan meydana gelir. Öncelikle gönderen kişinin hesabından göndermek istediği miktar düşülür, daha sonra bu miktar alıcı kişinin hesabına eklenir.

Bir örnek yapalım. "Hesap" adında bir tablo oluşturalım. "Id" (int), "TCKimlikNo" (char(11)), "Ad" (nvarchar(50)), "Bakiye" (money) kolonlarından meydana gelsin. Aşağıda görüldüğü gibi örnek için tabloya iki kayıt girmemiz yeterli.
Serdar 'dan Gaffar 'a "100 lira" para aktarım işlemi gerçekleştirelim. Fakat Serdar 'ın bakiyesi eksildikten sonra Gaffar 'ın bakiyesine ekleme işleminde hata verdirelim. Bu örnekteki senaryoda, hata verdirmek ve hatadan sonraki kod bloğunun çalışmaması için, kodlarımı "try-catch" kod blokları arasında yazıyorum. Öncelikle "transaction" kullanmadan ne gibi sorunlar yaşayacağım onu görelim.

BEGIN TRY
      UPDATE dbo.Hesap SET Bakiye-=100 WHERE TCKimlikNo='23456789101'
      RAISERROR('Elektrikler Kesildi',16,2)
      UPDATE dbo.Hesap SET Bakiye+=100 WHERE TCKimlikNo='12345678910'
END TRY
BEGIN CATCH
      PRINT 'Beklenmedik bir hata olustu'
END CATCH
Kodlarım çalıştı. Tabloma bakalım havale işlemim tamamlanmış mı?
SELECT * FROM dbo.Hesap
Görüldüğü üzere eksiltme işlemi gerçekleşti fakat ekleme işlemi tamamlanmadı. Ortada bir para kaybı söz konusu. Nerede bu para? Geçmiş olsun arkadaşlar, para kayıp. Şaka bir yana, işte bu yüzden "transaction" kullanıyoruz. "TRY" kod bloğunun içinde "BEGIN TRAN" ile "transaction" işlemini başlatıyorum. ("BEGIN TRANSACTION" da yazabilirdik.) Ard arda, tek bir işlemmiş gibi çalışmasını istediğim kod bloğumu yazdıktan sonra, "transaction" işlemini "COMMIT TRAN" (işlemi doğrulama anlamı var) ile sonlandırıyorum. "COMMIT TRAN" komutunu görene kadar, işlemlerimde hata olsa da olmasa da, işlem sonuçlarım tabloya yansımayacaktır. Daha sonra "try" bloğumu kapatıyorum. Bir hata ile karşılaşılması sonucunda "Catch" bloğu çalışacağından dolayı, "Catch" bloğunda da "ROLLBACK TRAN" komutu ile yapılan işlemleri geri alınmasını sağlıyoruz.

USE Deneme
BEGIN TRY
    BEGIN TRAN   
        UPDATE dbo.Hesap SET Bakiye-=100 WHERE TCKimlikNo='23456789101'
        RAISERROR('Elektrikler Kesildi',16,2)
        UPDATE dbo.Hesap SET Bakiye+=100 WHERE TCKimlikNo='12345678910'
    COMMIT TRAN
END TRY
BEGIN CATCH
    PRINT 'Beklenmedik bir hata olustu'
    ROLLBACK TRAN
END CATCH
Kodlarım çalıştı. Tabloma bakalım havale işlemim tamamlanmış mı?
SELECT * FROM dbo.Hesap
Görüldüğü üzere arkadaşlar istediğimiz gerçekleşti. Hata olması durumunda, gerçekleşen işlemler de geri alında. Program akışı "COMMIT TRAN" kodunu görseydi, işlemlerim başarılı bir şekilde gerçekleşmiş ve "Catch" bloğuna düşmemiş demekti. Bunun örneğini yapmaya gerek yok arkadaşlar. Zaten "transaction" işleminin çalışmasını anlamak için hata almam gerekiyordu.

Şimdi şöyle bir senaryo kurgulayalım. Bir kişi bankaya gelsin ve adına bir hesap açılmasını, bu hesabına da arkadaşından havale yapılmasını istesin. Hesap oluşturmak demek, "Hesap" tablosuna bir kayıt "insert" edilmesi gerek arkadaşlar. Daha sonra yukarıda yaptığımız eksiltme arttırma işlemleri. Peki bu işlemlerin hepsini "TRAN" blokları arasında yazarsam ne olur? Program akışı bir hata ile karşılaşırsa, hesap oluşturma işlemini de geri alacaktır. Ama biz hata ile karşılaşılması durumunda, hesap oluşturma işleminden sonraki işlemlerin geri alınmasını istiyoruz. Bunun için hesap oluştuktan sonra "transaction" işleminin kaydedilmesini sağlayabiliriz. Bu işlemi de "COMMIT SAVE TRAN BakiyeOlustu" kod satırı ile sağlarız. Bu satıra "transaction" işleminin kayıt noktası denir ve bir isim verilir. Biz hatırlanması kolay olması açısından "BakiyeOlustu" diye bir isim verdik. Daha sonra hata olursa "Catch" bloğunda, transaction içinde tanımladığımız işlemlerin "ROLLBACK TRAN" diyerek geri alınmasını değil de, bu oluşturduğumuz kayıt noktasına kadar olan işlemlerin geri alınması için "ROLLBACK TRAN BakiyeOlustu" kod satırını kullanacağız.

BEGIN TRY
    BEGIN TRAN   
        INSERT dbo.Hesap (TCKimlikNo, Ad, Bakiye) 
        VALUES ('34567891011', 'Castelli', 10000)
    COMMIT SAVE TRAN BakiyeOlustu
        UPDATE dbo.Hesap SET Bakiye-=100 WHERE TCKimlikNo='23456789101'
        RAISERROR('Elektrikler Kesildi',16,2)
        UPDATE dbo.Hesap SET Bakiye+=100 WHERE TCKimlikNo='12345678910'
    COMMIT TRAN
END TRY
BEGIN CATCH
    PRINT 'Beklenmedik bir hata olustu'
    ROLLBACK TRAN BakiyeOlustu
END CATCH
Kodlarım çalıştı. Tabloma bakalım hesap oluşturma ve havale işlemleri tamamlanmış mı?
SELECT * FROM dbo.Hesap
Evet arkadaşlar "transaction" işlemim tam da istediğimiz gibi başarılı bir şekilde çalıştı. Hesap oluşturma işlemi başarılı bir şekilde gerçekleşti. "1 satır etkilendi" mesajı ile de bunu anlıyoruz. Fakat havale işleminin ortasında bir hata meydana geldiğinden dolayı gerçekleşen kısımları da geri alındı. Yani  try bloğunda "COMMIT SAVE TRAN BakiyeOlustu" ve catch bloğunda "ROLLBACK TRAN BakiyeOlustu" demeseydik, hesap oluşturma işlemi de geri alınacaktı. Bu da istemediğimiz bir durumdu. Ya "commti" diyerek yada "rolback" diyerek "transaction" işlemini durduruyoruz.

Bu işlemleri daha dinamik hale getirmek için parametreleriyle birlikte bir "Strored Procedure" nesnesi oluşturup, kullanabiliriz. Gönderen ve alan kişilerin "TCKimlikNo" bilgilerini ve bir de gönderilmek istenen tutarı isteriz. "HavaleYap" adında bir "proc" tanımlıyoruz. 

USE Deneme
GO
CREATE PROC HavaleYap
(@GonderenTC NCHAR(11),@AlanTC NCHAR(11),@Tutar MONEY)
AS
BEGIN TRY
    BEGIN TRAN
        UPDATE dbo.Hesap SET Bakiye-=@Tutar WHERE TCKimlikNo=@GonderenTC
        RAISERROR('Elektrikler Kesildi',16,2)
        UPDATE dbo.Hesap SET Bakiye+=@Tutar WHERE TCKimlikNo=@AlanTC
    COMMIT TRAN
END TRY
BEGIN CATCH
    ROLLBACK TRAN
    PRINT 'Beklenmedik bir hata olustu'
END CATCH

Tanımladığımız bu "proc" nesnemizi çalıştıralım.
HavaleYap @GonderenTC='23456789101',@AlanTC='12345678910',@Tutar=100
Kodlarım çalıştı. Tabloma bakalım havale işlemi tamamlanmış mı?
SELECT * FROM dbo.Hesap
Görüldüğü üzere hata anında işlemlerim geri alındı. İstediğimiz gerçekleşti.
"Try" bloklarının içerisinde birden fazla "tran" başlatıp sonlandırabiliriz. "Catch" bloğunda bir tane "rollback" kullanarak yine işlemlerimi gerçekleştirebiliriz. Fakat aynı işlemi bir "tran" kullanıp, istediğimiz yere "save point" atmak varken, iki "tran" kullanmaya hiç gerek yok arkadaşlar.

Umarım yardımcı olabilmişimdir. Bir başka yazıda görüşmek dileğiyle, hoşçakalın...

5 yorum: