קינון SqlTransaction

נוסף ב-20/08/2007 08:52 על ידי דניאל כץ

הבעיה שבה נתקלתי היא, למצוא דרך נוחה להשתמש באותה הפונקציה (בדוגמה שלנו זה ‏BasicPattern‏) בתוך ומחוץ ל‏SqlTransaction‏ בלי לשכתב אותה פעמיים.‏

מאילוצי המבנה של ספריות ה‎.NET‎‏ יש צורך לכתוב כל אחת מהפונקציות האלה (ובמקרים רבים יש הרבה כאלה) פעמיים: פעם אחת תוך פתיחת חיבור חדש למסד הנתונים, ופעם שנייה תוך שימוש בחיבור שכבר נפתח עבור הטרנסקציה ע"י הפונקציה הקודמת (בדוגמה שלנו זה ‏SimpleTransactionPattern‏).

הפתרון שלי בא בצורת מחלקה חדשה שהענקתי לה את השם ‏DbConnection‏. במחלקה קיימים שני ‏Constructorים:

  1. New(ByVal enforceTransaction As Boolean)‎
  2. New(ByVal connection As DbConnection, ByVal enforceTransaction As Boolean)

המתאימים לאתחול המחלקה כמופע ראשוני או כמופע מקונן. מופע ראשוני הוא זה שבעצם פתח את החיבור ורק הוא יוכל לסגור אותו. להבדיל, המופע מקונן ישתמש בחיבור ובטרנסקציה (אם קיימת) של המופע הראשוני ולא יוכל לסגור את החיבור.
הבנאי הראשון מקבל רק פרמטר אחד שקובע אם צריך לפתוח טרנסקציה. הבנאי תמיד יבנה מופע ראשוני של המחלקה. הבנאי השני דומה לבנאי הראשון בהבדל אחד, שאם מעבירים לו כפרמטר מופע אחר של המחלקה הוא יבנה מופע מקונן של המחלקה.

דוגמא: להדגמה כתבתי מחלקה (‏TestDbConnection‏) שממחישה את הגישה של לפני הפתרון שמצריך העמסה (כפל) של הפונקציה ‏BasicPattern‏ תוך שכפול הקוד שלה, מול הגישה החדשה תוך שימוש במחלקת ‏DbConnection‏ שכתבתי (פונקציות ‏UsingDbConnectionRoot‏ ו‏UsingDbConnectionSub‏). בנוסף הדגמתי בפונקציה ‏DbConnectionRoot‏ ממשוק של המחלקה שלי לשיטה המסורתית.

בנוסף: המחלקה מממשת קינון טרנסקציות, כלומר אם אחת מהפעולות שנעשות בתוך הטרנסקציה הראשונה מצריכה טרנסקציה בפני עצמה. יהיה ניתן להשיג זאת ע"י קריאה לבנאי השני עם פרמטר ‏enforceTransaction‏ שווה "אמת", ולקבל טרנסקציה חדשה מקוננת בתוך הראשונה ומנוהלת ע"י המחלקה כמו שמודגם בפונקציה ‏UsingDbConnectionNestedTransactionTest‏.

חוקים:
  1. רק מופע ראשוני יכול לסגור את החיבור למסד הנתונים.
  2. מופע ראשוני או מקונן שפתח טרנסקציה חדשה חייב לסיים אותה (‏Rollback‏ או ‏Commit‏) בעודו "חי", אחרת הטרנסקציה תבוטל (‏Rollback‏).
  3. מופע מקונן שירש טרנסקציה, לא שולט על מצבה ויתעלם מקריאות לפונקציות ‏RollbackTransaction, CommitTransaction, SaveTransaction‏.

Public Class TestDbConnection

 

    Public Sub TestAllApproaches()

 

        BasicPattern()

        SimpleTransactionPattern()

 

        UsingDbConnectionSub() ' Simple call

        UsingDbConnectionRoot() ' Cascade call

        UsingDbConnectionNestedTransactionTest() ' Nested sql transactions test

 

    End Sub

 

    Public Sub BasicPattern()

        Using conn As New SqlConnection(Common.GetConnectionString)

            conn.Open()

 

            Dim comm As New SqlCommand("Users_DoStuff", conn)

            comm.CommandType = Data.CommandType.StoredProcedure

            comm.ExecuteNonQuery()

        End Using

    End Sub

 

    Public Sub BasicPattern(ByVal trans As SqlTransaction)

        Dim comm As New SqlCommand("Users_DoStuff", trans.Connection, trans)

        comm.CommandType = Data.CommandType.StoredProcedure

        comm.ExecuteNonQuery()

    End Sub

 

    Public Sub SimpleTransactionPattern()

        Using conn As New SqlConnection(Common.GetConnectionString)

            conn.Open()

 

            Dim trans As SqlTransaction = conn.BeginTransaction

            Dim comm As New SqlCommand("Users_BeginDoingStuff", conn, trans)

            comm.CommandType = Data.CommandType.StoredProcedure

            comm.ExecuteNonQuery()

 

            BasicPattern(trans)

        End Using

    End Sub

 

    Public Sub UsingDbConnectionRoot()

        Using conn As New DbConnection(True)

            Dim comm As SqlCommand = conn.CreateCommand("Users_BeginDoingStuff")

            comm.CommandType = Data.CommandType.StoredProcedure

            comm.ExecuteNonQuery()

 

            BasicPattern(conn.Transaction)

            UsingDbConnectionSub(conn)

        End Using

    End Sub

 

    Public Sub UsingDbConnectionSub(Optional ByVal transaction As DbConnection = Nothing)

        Using conn As New DbConnection(False, transaction)

            Dim comm As SqlCommand = conn.CreateCommand("Users_DoStuff")

            comm.CommandType = Data.CommandType.StoredProcedure

            comm.ExecuteNonQuery()

        End Using

    End Sub

 

    Public Sub UsingDbConnectionNestedTransactionTest()

        Using conn As New DbConnection(True)

            Dim comm As SqlCommand = conn.CreateCommand("Users_BeginDoingStuff")

            comm.CommandType = Data.CommandType.StoredProcedure

            comm.ExecuteNonQuery()

 

            ' New transaction nested on the first one

            Using connNested As New DbConnection(True, conn)

                Dim commNested As SqlCommand = conn.CreateCommand("Users_BeginDoingStuff")

                commNested.CommandType = Data.CommandType.StoredProcedure

                commNested.ExecuteNonQuery()

 

                ' Roll's back the first transaction too

                connNested.RollbackTransaction()

            End Using

        End Using

    End Sub

End Class

וזה הקוד של המחלקה עצמה:‏

Imports System.Data.SqlClient

Imports Common

 

Public Class DbConnection

    Implements IDisposable

 

    Protected ReadOnly m_connection As SqlConnection

    Protected ReadOnly m_transactions As Stack(Of SqlTransaction)

    Protected m_transactionStatus As TransactionStatusEnum

 

    Protected m_open As Boolean

    Protected m_disposed As Boolean

    Protected ReadOnly m_disposable As Boolean

 

    Public Sub New(ByVal enforceTransaction As Boolean)

        Me.New(enforceTransaction, Nothing)

    End Sub

 

    Public Sub New(ByVal enforceTransaction As Boolean, ByVal connection As DbConnection)

        m_transactionStatus = TransactionStatusEnum.Inherited

        m_disposed = False

 

        If connection Is Nothing Then

            m_open = False

            m_disposable = True

 

            m_transactions = New Stack(Of SqlTransaction)

            m_connection = New SqlConnection(Common.GetConnectionString)

 

            Me.OpenConnection()

 

            If enforceTransaction Then

                Me.BeginTransaction()

            End If

        Else

            If Not connection.m_disposed Then

                m_connection = connection.m_connection

                m_transactions = connection.m_transactions

                m_open = connection.m_open

 

                m_disposable = False

            Else

                Throw New ObjectDisposedException(Me.GetType.ToString)

            End If

        End If

    End Sub

 

    Protected Sub OpenConnection()

        If Not m_disposed Then

            If Not m_open Then

                m_open = True

                m_connection.Open()

            End If

        Else

            Throw New ObjectDisposedException(Me.GetType.ToString)

        End If

    End Sub

 

    Protected Sub BeginTransaction()

        If Not m_disposed Then

            m_transactions.Push(m_connection.BeginTransaction())

            m_transactionStatus = TransactionStatusEnum.Open

        Else

            Throw New ObjectDisposedException(Me.GetType.ToString)

        End If

    End Sub

 

    Protected ReadOnly Property CurrentTransaction() As SqlTransaction

        Get

            If m_transactions.Count > 0 Then

                Return m_transactions.Peek()

            Else

                Return Nothing

            End If

        End Get

    End Property

 

    Public Function CreateCommand(ByVal str As String) As SqlCommand

        If Not m_disposed Then

            If CurrentTransaction Is Nothing Then

                Return New SqlCommand(str, m_connection)

            Else

                Return New SqlCommand(str, m_connection, CurrentTransaction)

            End If

        Else

            Throw New ObjectDisposedException(Me.GetType.ToString)

        End If

    End Function

 

    Public Sub RollbackTransaction()

        If Not m_disposed Then

            If m_transactionStatus = TransactionStatusEnum.Open Then

 

                While m_transactions.Count > 0

                    m_transactions.Pop().Rollback()

                End While

                m_transactionStatus = TransactionStatusEnum.RolledBack

 

            ElseIf m_transactionStatus = TransactionStatusEnum.Commited _

                    Or m_transactionStatus = TransactionStatusEnum.RolledBack Then

 

                Throw New InvalidOperationException( _

                    "Cannot rollback commited or rolledback transaction")

            End If

        Else

            Throw New ObjectDisposedException(Me.GetType.ToString)

        End If

    End Sub

 

    Public Sub SaveTransaction(ByVal savePointName As String)

        If Not m_disposed Then

            If m_transactionStatus = TransactionStatusEnum.Open Then

 

                m_transactions.Peek().Save(savePointName)

 

            ElseIf m_transactionStatus = TransactionStatusEnum.Commited _

                    Or m_transactionStatus = TransactionStatusEnum.RolledBack Then

 

                Throw New InvalidOperationException( _

                    "Cannot save rolledback or commited transaction")

            End If

        Else

            Throw New ObjectDisposedException(Me.GetType.ToString)

        End If

    End Sub

 

    Public Sub CommitTransaction()

        If Not m_disposed Then

            If m_transactionStatus = TransactionStatusEnum.Open Then

 

                m_transactions.Peek().Commit()

                m_transactions.Pop()

                m_transactionStatus = TransactionStatusEnum.Commited

 

            ElseIf m_transactionStatus = TransactionStatusEnum.Commited _

                    Or m_transactionStatus = TransactionStatusEnum.RolledBack Then

                Throw New InvalidOperationException( _

                    "Cannot commit commited or rolledback transaction")

            End If

        Else

            Throw New ObjectDisposedException(Me.GetType.ToString)

        End If

    End Sub

 

    <Obsolete("Exists only for standart framework compatibility but unsafe to use")> _

    Public ReadOnly Property Transaction() As SqlTransaction

        Get

            Return CurrentTransaction

        End Get

    End Property

 

    Public Sub Dispose() Implements IDisposable.Dispose

        If Not m_disposed Then

            If m_disposable Then

                If m_transactions.Count > 0 Then

                    Throw New InvalidOperationException( _

                        "DbConnection disposed before all transaction are commited.")

                End If

 

                m_connection.Dispose()

                m_disposed = True

            Else

                If m_transactionStatus = TransactionStatusEnum.Open Then

 

                    ' if the transaction whos'nt commited

                    RollbackTransaction()

                End If

            End If

        End If

    End Sub

 

    Protected Enum TransactionStatusEnum

        Inherited

        Open

        Commited

        RolledBack

    End Enum

 

End Class

Tags: ,

.NET

הוסף תגובה


(יציג את האייקון ה-Gravatar שלך)

  Country flag

biuquote
  • הערה
  • תצוגה מקדימה
Loading