COM+ Component'i oluşturmadan COM+ Servislerinden Faydalanmak - 2
COM+ Component'i oluşturmadan COM+ Servislerinden Faydalanmak makalemizin birinci bölümünde COM+ ile transaction servisini COM+ componenti oluşturmadan nasıl kullanacağımız incelemiş ve örneklendirmiştik.
Bu makalemizde ise COM+ Transaction servisinden faydalanarak farklı sınıflar içerisinde çalıştırılan birbirinden bağımsız ancak iş kuralı olarak tek iş olan işlemleri tek bir transaction içerisinde çalıştıracağız. Sipariş kayıdı girilirken Siparisler tablosuna kaydedilen sipariş bilgisi ile SiparisDetaylar tablosuna kaydedilen sipariş içerisindeki ürünleri belirleyen kayıtların tek bir transaction içerisinde yer almasını sağlayacağız.
Not: Bu senaryoda ki gibi aynı veritabanında bulunan iki tablo üzerinde yapılan işlemler zaten ADO.NET Transaction ile yapılabilir ve o şekilde yapılması performans açısından tavsiye edilmektedir. Ancak burada örneklendirilmek istenen farklı sınıflar içerisinde birbirinden tamamen habersiz olarak gerçekleştirilen ve DTC tarafından yönetilebilen işlerin aynı transactiona dahil edilmesidir. Kendi uygulamalarınız da uzakdaki bir sunucu üzerinde test etmek için sadece connectionstring değiştirerek ve makale sonunda bahsedilen yapılandırmayı yaparak bu işlemi gerçekleştirebilirsiniz.
Örnek uygulama tablo yapısı
Yukarıdaki şemaya uygun olarak verilerimizi taşıyacak ve veriler üzerinde CRUD (CREATE, READ, UPDATE, DELETE) işlemlerini gerçekleştirecek olan sınıflarımızı oluşturalım. Burada basitlik açısından sınıf tasarımlarında eksikler vardır. Şuan ki amacımız için gerekli olan özellik ve metodlar dahil edilmiştir.
Public Class Musteri Private _MusteriID As Integer Private _Ad As String Private _Email As String Public Property MusteriID() As Integer Get Return _MusteriID End Get Set(ByVal Value As Integer) _MusteriID = Value End Set End Property Public Property Ad() As String Get Return _Ad End Get Set(ByVal Value As String) _Ad = Value End Set End Property Public Property Email() As String Get Return _Email End Get Set(ByVal Value As String) _Email = Value End Set End Property End Class
Musteri sınıfı veritabanındaki müşteri bilgilerini taşımak için gerekli olan sınıftır. Örneğimiz için gerekli olmayan CRUD işlemleri için herhangi bir implementasyon yapılmamıştır.
Public Class SiparisDetay
Private _Siparis As Siparis Private _Miktar As Integer Private _BirimFiyat As Decimal Private _UrunID As Integer
Public Property Siparis() As Siparis 'kodun sadeliği için silinmiştir End Property Public Property Miktar() As Integer 'kodun sadeliği için silinmiştir End Property Public Property BirimFiyat() As Decimal 'kodun sadeliği için silinmiştir End Property Public Property UrunID() As Integer 'kodun sadeliği için silinmiştir End Property Public Sub New() End Sub Public Sub New(ByVal Miktar As Integer, ByVal BirimFiyat As Decimal, ByVal UrunID As Integer) Me.Miktar = Miktar Me.BirimFiyat = BirimFiyat Me.UrunID = UrunID End Sub
Public Sub Kaydet() Dim dal As New SqlDal dal.Params.Add("@SiparisID", Me.Siparis.SiparisID) dal.Params.Add("@UrunID", Me.UrunID) dal.Params.Add("@Miktar", Me.Miktar) dal.Params.Add("@BirimFiyat", Me.BirimFiyat) dal.ExecuteNonQuery("INSERT INTO SiparisDetaylar (SiparisID, UrunID, Miktar, BirimFiyat) VALUES (@SiparisID, @UrunID, @Miktar, @BirimFiyat)") End Sub
End Class
Yukarıdaki kod bloğun yer alan sınıf bir siparis detay kayıdını temsil eden ve gerekli olan yeni kayıt eklemeyi sağlayan bir metodu (kaydet) içermektedir. Kod bloğu içerisinde yer alan property'lerin standart set ve get bölümleri makale içerisinde kodun sadeliğini sağlaması açısında silinmiştir. Örnek uygulama içerisinde ilgili bölümler tam ve çalışır halde yer almaktadır.
Bu sınıf kayıt işlemi için SqlDal Adındaki veri erişim sınıfını kullanmaktadır bu sınıf ile yapılan işlemler yeni bir connection üzerinden yapılmaktadır.
Imports System.Data.SqlClient Imports System.EnterpriseServices Public Class Siparis Private _SiparisDetaylar As New SiparisDetayCollection(Me) Private _SiparisID As Integer Private _Tarih As Date Private _Musteri As Musteri
Public Property SiparisDetaylar() As SiparisDetayCollection 'kodun sadeliği için silinmiştir End Property Public Property SiparisID() As Integer 'kodun sadeliği için silinmiştir End Property Public Property Tarih() As Date 'kodun sadeliği için silinmiştir End Property Public Property Musteri() As Musteri 'kodun sadeliği için silinmiştir End Property
Public Sub Kaydet() Try Dim config As New ServiceConfig config.Transaction = TransactionOption.RequiresNew config.TrackingEnabled = True config.TrackingAppName = "Sipariş Bileşeni" config.TrackingComponentName = "Siparis" ServiceDomain.Enter(config) '---- Dim dal As New SqlDal dal.Params.Add("@MusteriID", Me.Musteri.MusteriID) dal.Params.Add(New SqlParameter("@Tarih", SqlDbType.DateTime)) dal.Params(1).Value = Me.Tarih Me.SiparisID = CType(dal.ExecuteScalar("INSERT INTO Siparisler (MusteriID, Tarih) VALUES (@MusteriID, @Tarih);SELECT SCOPE_IDENTITY();"), Integer)
For Each sipDetay As SiparisDetay In Me.SiparisDetaylar &n bsp; sipDetay.Kaydet() 'sipariş içindeki sipariş detayları kaydet Next ContextUtil.SetComplete() 'işler yolunda tüm işlemleri onayla Catch ex As Exception ContextUtil.SetAbort() 'işler yolunda gitmedi transaction içindeki tüm işleri geri al Throw ex Finally ServiceDomain.Leave() End Try End Sub
Yukarıdaki kod içerisinde bir siparişi temsil etmek ve yeni bir sipariş kayıdı girebilmeyi sağlayan bir sınıf yer almaktadır. Bu sınıf içerisindeki Kaydet metodu dikkat etmemiz gerek bölümdür. Bu metod içerisinde önce kendi connectionı ile bir sipariş kayıdı girilmekte (INSERT INTO) daha sonra bu siparis nesnesinin SiparisDetaylar collectionı içerisideki tüm siparisdetay sınıflarının kaydet metodu çağrılmaktadır.
Ve bütün bu işlemle COM+ servislerinden faydalanabilmek için bir context oluşturulduğu sırada yapıldığı için yapılan tüm bu işlemler bir COM+ transactionı içerisinde yer almaktadır.
Yukarıdaki siparis sınıfı test etmek için bir windows form üzerine bir buton ekleyip aşağıdaki kodu yazınız.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click Try Dim mus As New Musteri mus.Ad = "Cengiz" : mus.Email = "cengiz@cengizhan.com" mus.MusteriID = 1 Dim sip As New Siparis sip.Tarih = Now sip.Musteri = mus 'ilk ürün Dim sipdetay As SiparisDetay = New SiparisDetay sipdetay.BirimFiyat = 100 sipdetay.Miktar = 1 sipdetay.UrunID = 1 sip.SiparisDetaylar.Add(sipdetay) 'ikinci ürün sip.SiparisDetaylar.Add(New SiparisDetay(2, 200, 2)) 'siparişi kaydet sip.Kaydet() Catch ex As Exception MsgBox(ex.Message) End Try End Sub
Yukarıdaki kodun click olayına yazıldığı butona tıklandığında aşağıdaki resimde gözüktüğü gibi bir COM+ Transactionı başlatılır.
Bu durumda COM+ Transactionı içerisinde yapılan sipariş kayıdı girme, sipariş için birinci sipariş detayını girme, sipariş için ikinci sipariş detayını girme işlemlerin hepsi onaylandığı takdirde işlemler ilgili veri kaynaklarına yansıtılacak aksi halde DTC servisi aracılığı ile işlemler onaylanmadan, geri alınacaktır.
Aslında bu örnek uygulama ile .NET Framework 2.0'da gelen ve heyecan uyandıran TransactionScope sınıfın yaptığı işlemin aynısı uygulamış oluyoruz.
Bu senaryoda kullanılan yöntemleri farklı konumlardaki veri kaynakları üzerinde çalıştırarak test edebilirsiniz. Örneğin Siparisler ve SiparisDetaylar tabloları farklı SQL Server'lar üzerinde oluşturulup(tamam pek gerçekçi değil:)), connectionstringler düzenlenip aynı işlem test edilebilir ve bu şekilde Distributed Transaction gerçekleştirilmiş olur.
Ancak...
DTC'nin farklı konumlardaki DTC'ler ile konuşabilmesi için DTC'nin Network Access'e sahip olması gerekir. Bu ayar varsayılan olarak kapalı gelir ve açılması gerekir. Aksi halde DTC'nin "enlist" olurken hata ile karşılaştığını anlatan bir hata mesajı alırsınız.
Bunun için;
Başlat/Çalıştır'a dcomcnfg yazıp Component Services yönetim aracını açın ve Component Services>Computers>My Computer üzerine sağ tıklayıp Properties seçeneğini seçin. Açılan diyalog penceresinden MSDTC sekmesine geçin, oradan da Security Configuration butonuna tıklayın.
DTC'nin network üzerinden işlem yapabilmesini sağlayın.
Yukarıdaki anlatılan adımların Distributed Transaction'a dahil olacak tüm bilgisayarlardaki DTC servisleri için gerçekleştirilmesi gereklidir. Yani hem koordinasyonu sağlayan (coordinating) hemde katılımcı (participating) DTC servileri üzerinde bu işlem gerçekleştirilmelidir.
Not: Çalıştığınız işletim sistemine bağlı olarak Security Configuration penceresi farklı bir tasarım ile karşınıza gelebilir. Bu işlem ile ilgili olarak makalenin sonunda yer alan Microsoft Knowledge Base "How To" sunu okumanızı öneririm.
Ek kaynak : Windows Server 2003'te ağ DTC erişimi nasıl etkinleştirilir
Cengiz HAN Microsoft ASP.NET MVP cengiz@cengizhan.com
Uygulamada yer alan diğer sınıflar :
Imports System.Data.SqlClient
Public Class SqlDal
Private _ConnectionString As String
Private _Params As New SqlDalParameterCollection
Public Property Params() As SqlDalParameterCollection
Get
Return _Params
End Get
Set(ByVal Value As SqlDalParameterCollection)
_Params = Value
End Set
End Property
Public Property ConnectionString() As String
Return _ConnectionString
Set(ByVal Value As String)
_ConnectionString = Value
Public Sub New()
Dim strCon As String = Configuration.ConfigurationSettings.AppSettings("strCon")
If strCon Is Nothing Then
Me.ConnectionString = "server=.;database=makaleDB;user id=sa;password=;"
Else
Me.ConnectionString = strCon
End If
End Sub
Public Sub New(ByVal ConnectionString As String)
Me.ConnectionString = ConnectionString
Public Function ExecuteNonQuery(ByVal sql As String) As Integer
Return ExecuteNonQuery(sql, CommandType.Text)
End Function
Public Function ExecuteNonQuery(ByVal sql As String, ByVal cmdType As CommandType) As Integer
Dim con As New SqlConnection(Me.ConnectionString)
Dim cmd As New SqlCommand(sql, con)
cmd.CommandType = cmdType
AddParams(cmd)
con.Open()
Dim affected As Integer = cmd.ExecuteNonQuery()
con.Close()
Return affected
Private Sub AddParams(ByVal cmd As SqlCommand)
For Each param As SqlParameter In Me.Params
cmd.Parameters.Add(param)
Next
Public Function ExecuteReader(ByVal sql As String) As SqlDataReader
Return ExecuteReader(sql, CommandType.Text)
Public Function ExecuteReader(ByVal sql As String, ByVal cmdType As CommandType) As SqlDataReader
Dim rdr As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
Return rdr
Public Function ExecuteScalar(ByVal sql As String) As Object
Return ExecuteScalar(sql, CommandType.Text)
Public Function ExecuteScalar(ByVal sql As String, ByVal cmdType As CommandType) As Object
Dim obj As Object = cmd.ExecuteScalar
Return obj
Public Class SqlDalParameterCollection
Inherits CollectionBase
Public Function Add(ByVal param As SqlParameter) As Integer
Return List.Add(param)
Public Function Add(ByVal name As String, ByVal value As Object) As SqlParameter
Dim param As New SqlParameter
param.ParameterName = name
param.Value = value
Me.List.Add(param)
Return param
Default Public Property Item(ByVal index As Integer) As SqlParameter
Return CType(List(index), SqlParameter)
Set(ByVal Value As SqlParameter)
List(index) = Value
Public Class SiparisDetayCollection
Private _siparis As Siparis
Public Sub New(ByVal siparis As Siparis)
_siparis = siparis
Public Function Add(ByVal sipdetay As SiparisDetay) As Integer
sipdetay.Siparis = _siparis
Return List.Add(sipdetay)
Default Public Property Items(ByVal index As Integer) As SiparisDetay
Return CType(List(index), SiparisDetay)
Set(ByVal Value As SiparisDetay)