VB6で(なんちゃって)宣言的トランザクション
前回に引き続きVB6でも、できるだけの事はやってみようシリーズ
VB6でデータベースを使ったアプリケーションでよくやってしまうのは、トランザクションの張り忘れとかロールバック、コミットのし忘れなど。おまけに汎用的な関数の中でやっちゃったりして、トランザクションがまともに機能しなかったりする。
こういうのはきっちりルールを決めて実装しなくちゃいけないんだけど、なかなか守れないのが実情。そういう状況を打破するために宣言するだけでトランザクションの開始とコミット、ロールバックが自動で行われるモジュールを作ってみた。
使い方はこんな感じ
modMain.bas
Sub Main() Call modException_ClearLastException() On Error Goto EXCEPT ' modTransaction_Beginでトランザクションが開始される ' また、引数に接続オブジェクトを渡す事もできる With modTransaction_Begin ' Connectionプロパティで接続オブジェクトが取れるので ' それを使って何らかのDBアクセスを行う Call DoSomething(.Connection) ' ' ' ' また、明示的にロールバックする場合はRollbackメソッドを呼び出す If Condition() Then .Rollback End With ' Withブロックを抜けた時点でコミットされる ' Withブロック内でエラーが発生した場合は、ロールバックされる Exit Sub EXCEPT: With modException_GetLastException() Debug.Print .ToString MsgBox .Description, vbExclamation End With End Sub
modTransaction_Begin関数を呼び出すとclsTransactionクラスというトランザクションを制御するクラスのインスタンスが返ってくる。
このインスタンスをWithブロックを使って生存期間を限定してやる事でトランザクションを張る場所を制御できる。
仕掛けとしてはこう
以下がクラスの実装
clsTransaction.cls
Option Explicit '#--------------------------------------------------------------------------- '# クラス名:clsTransaction '# 概要 :トランザクションに関する操作を実行するクラス '#--------------------------------------------------------------------------- Private m_objCon As ADODB.Connection '#--------------------------------------------------------------------------- '# プロパティ名:Connection '# 機能 :接続オブジェクトを取得します。 '# '# 戻り値:接続オブジェクト '#--------------------------------------------------------------------------- Public Property Get Connection() As ADODB.Connection Set Connection = m_objCon End Property Private Sub Class_Initialize() If modTransaction_Connection Is Nothing Then Call modException_Throw(InvalidOperation, "clsTransaction.Class_Initialize", _ "不正な操作です。[clsDao]のインスタンス化には[clsDao_New]関数を使用して下さい。" _ ) End If Set m_objCon = modTransaction_Connection Me.Begin End Sub Private Sub Class_Terminate() If modException_ExsitsException() Then If Err.Number = 0 Then Me.Commit Else Me.ROLLBACK End If End If End Sub '#--------------------------------------------------------------------------- '# 関数名:Begin '# 機能 :トランザクションを開始します。 '# '# 戻り値:開始に成功したかどうか '#--------------------------------------------------------------------------- Public Function Begin() As Boolean If Not m_objCon Is Nothing Then Call m_objCon.BeginTrans Begin = True End If End Function '#--------------------------------------------------------------------------- '# 関数名:Commit '# 機能 :トランザクションをコミットします。 '# '# 戻り値:コミットに成功したかどうか '#--------------------------------------------------------------------------- Public Function Commit() As Boolean If Not m_objCon Is Nothing Then Call m_objCon.CommitTrans Commit = True End If End Function '#--------------------------------------------------------------------------- '# 関数名:Rollback '# 機能 :トランザクションをロールバックします。 '# '# 戻り値:ロールバックに成功したかどうか '#--------------------------------------------------------------------------- Public Function ROLLBACK() As Boolean If Not m_objCon Is Nothing Then Call m_objCon.RollbackTrans ROLLBACK = True End If End Function '#--------------------------------------------------------------------------- '# 関数名:GetDao '# 機能 :Daoオブジェクトを取得します。 '# '# 戻り値:Daoオブジェクト '#--------------------------------------------------------------------------- Public Function GetDao() As clsDao If Not m_objCon Is Nothing Then Set GetDao = clsDao_New(m_objCon) End If End Function
これに対応するモジュール
modTransaction
Option Explicit ' clsTransactionのコンストラクタで接続オブジェクトを取得する為に ' 一時的に参照する為の変数、それ以外の目的では使用しない事 Public modTransaction_Connection As ADODB.Connection '#--------------------------------------------------------------------------- '# 関数名:modTransaction_Begin '# 機能 :指定した接続でトランザクションを開始します。 '# '# 引数 :objCon: 接続オブジェクト(省略時はカレントプロジェクト) '# '# 戻り値:トランザクションオブジェクト '#--------------------------------------------------------------------------- Public Function modTransaction_Begin(Optional ByRef objcon As ADODB.Connection) As clsTransaction If objcon Is Nothing Then Set modTransaction_Connection = CurrentProject.Connection Else Set modTransaction_Connection = objcon End If On Error Resume Next Set modTransaction_Begin = New clsTransaction Set modTransaction_Connection = Nothing End Function
これを使う事でコミットやロールバックのし忘れを防ぐ事もできるし、トランザクションの期間を明確にできる。
あとは、これを最上位のモジュールから呼び出すという事をルール付ければいい。
おまけでこういうのも作った
modMain.bas
Sub Main() Dim objRs As RecordSet Dim varResult As Variant Dim intRecords As Integer With modTransaction_Begin ' clsDaoクラスのインスタンスが返ってくる With .GetDao() ' レコードセットを返すメソッド Set objRs = .Execute("SELECT * FROM HogeTable") ' 単一の結果が返ってくる varResult = .ExecuteScalar("SELECT COUNT(*) FROM HogeTable") ' 影響を受けた行数が返ってくる intRecords = ExecuteNonQuery("DELETE FROM HogeTable") End With End With ' このユーティリティ関数でもインスタンス可できる ' 引数に接続オブジェクトを渡す事も可能 With clsDao_New ' DoSomething End With End Sub
ADO.NETのインターフェースをそのままぱくったDAOクラス(clsDao)、それなりに便利かもしれない。
以下、ソース
clsDao.cls
Option Explicit '#--------------------------------------------------------------------------- '# クラス名:clsDao '# 概要 :データベースへのアクセスに関する操作を提供するクラス '#--------------------------------------------------------------------------- Private m_objCon As ADODB.Connection Private m_blnIsCreateNew As Boolean '#--------------------------------------------------------------------------- '# プロパティ名:Connection '# 機能 :接続オブジェクトを取得します。 '# '# 戻り値:接続オブジェクト '#--------------------------------------------------------------------------- Public Property Get Connection() As ADODB.Connection Set Connection = m_objCon End Property Private Sub Class_Initialize() If modDao_Connection Is Nothing Then Call modException_Throw(InvalidOperation, "clsDao.Class_Initialize", _ "不正な操作です。[clsDao]のインスタンス化には[clsDao_New]関数を使用して下さい。" _ ) End If Set m_objCon = modDao_Connection m_blnIsCreateNew = modDao_IsCreateNew End Sub Private Sub Class_Terminate() If Not m_objCon Is Nothing And m_blnIsCreateNew Then Call m_objCon.Close End If End Sub '#--------------------------------------------------------------------------- '# 関数名:Execute '# 機能 :指定したSQLでデータベースに問い合わせを行います。 '# '# 引数 :strSql: SQL '# :cursorType: [Option]カーソルの種類 '# :lockType: [Option]ロックの種類 '# '# 戻り値:結果オブジェクト '#--------------------------------------------------------------------------- Public Function Execute( _ ByVal strSql As String, _ Optional cursorType As CursorTypeEnum = CursorTypeEnum.adOpenStatic, _ Optional lockType As LockTypeEnum = LockTypeEnum.adLockReadOnly) As Recordset Dim objRs As New Recordset Call objRs.Open(strSql, Me.Connection, cursorType, lockType) Set Execute = objRs End Function '#--------------------------------------------------------------------------- '# 関数名:ExecuteScalar '# 機能 :指定したSQLでデータベースに問い合わせを行います(結果が単一の値)。 '# '# 引数 :strSql: SQL '# :cursorType: [Option]カーソルの種類 '# :lockType: [Option]ロックの種類 '# '# 戻り値:結果の値 '#--------------------------------------------------------------------------- Public Function ExecuteScalar( _ ByVal strSql As String, _ Optional cursorType As CursorTypeEnum, _ Optional lockType As LockTypeEnum) As Variant With Execute(strSql, cursorType, lockType) If .EOF Then Call modException_Throw(InvalidArgument, _ "clsDao.ExecuteSingle", "返却されるレコードが存在しません。" _ ) End If ExecuteScalar = .Fields(0).Value End With End Function '#--------------------------------------------------------------------------- '# 関数名:ExecuteNonQuery '# 機能 :指定したSQLでデータベースに問い合わせを行います(影響を受けた行数を返却)。 '# '# 引数 :strSql: SQL '# '# 戻り値:影響を受けた行数 '#--------------------------------------------------------------------------- Public Function ExecuteNonQuery(ByVal strSql As String) As Integer Dim intRecords As Integer Call Connection.Execute(strSql, intRecords) ExecuteNonQuery = intRecords End Function
対応するモジュール
modDao.bas
Option Explicit ' clsDaoのコンストラクタで接続オブジェクトを取得する為に ' 一時的に参照する為の変数、それ以外の目的では使用しない事 Public modDao_Connection As ADODB.Connection ' Connectionが新しく作られたかどうか Public modDao_IsCreateNew As Boolean '#--------------------------------------------------------------------------- '# 関数名:clsDao_New '# 機能 :指定した接続でDAOオブジェクトを生成します。 '# '# 引数 :objCon: 接続オブジェクト(省略時はカレントプロジェクト) '# '# 戻り値:DAOオブジェクト '#--------------------------------------------------------------------------- Public Function clsDao_New(Optional ByRef objcon As ADODB.Connection) As clsDao If objcon Is Nothing Then Set modDao_Connection = CurrentProject.Connection modDao_IsCreateNew = True Else Set modDao_Connection = objcon modDao_IsCreateNew = False End If On Error Resume Next Set clsDao_New = New clsDao Set modDao_Connection = Nothing End Function
こういうのを用意して、プロジェクトメンバーに周知させれば、一定の品質を保てるようになればいいなぁ...