データ不整合という名の致命傷を避ける――DAOトランザクションにおける「完璧なロールバック」の設計思想
業務アプリケーションの開発において、最も恐ろしいのは「エラーでプログラムが止まること」ではない。「エラーが起きたのに、データの一部だけが書き換わってしまうこと」だ。
例えば、在庫テーブルから数量を減らし、売上テーブルにレコードを追加する処理があるとしよう。在庫の減算には成功したが、売上の追加時にネットワークエラーや制約違反で失敗したとする。この時、在庫だけが減った状態――これを我々は「データ不整合」と呼び、システムの信頼性を根底から破壊する致命的なバグと定義する。
プロフェッショナルのコードは、成功のルートだけでなく、「失敗した時にいかに美しく、元の状態へ戻るか」にこそ、その真価が宿る。
本稿では、Access VBAにおけるDAO(Data Access Objects)を用いた、堅牢なトランザクション処理の定型句と、その背後にある設計思想を伝授する。
1. なぜ「Updateメソッド単体」の思考では足りないのか
多くの初学者は、`Recordset.Update` を呼び出し、エラーが出たら `MsgBox` で通知して終了する、という設計を書きがちだ。しかし、これは「単一の操作」しか考えていない、極めて危険な設計である。
実務における「業務処理」は、常に複数のテーブル、あるいは複数のレコードに対する一連の「不可分なステップ」の集合体である。
ここで重要になるのが、ACID特性における「原子性(Atomicity)」だ。
「すべて実行されるか、さもなくば、全く実行されないか」のどちらか一方でなければならない。この原子性を担保するための仕組みがトランザクションである。
DAOとADO:アーキテクチャの視点
Access開発において、ローカルまたは分割されたAccessデータベースを操作する場合、DAOはADOよりも圧倒的に低レイヤーで動作し、Jet/ACEエンジンに直結している。そのため、パフォーマンスに優れ、トランザクションの制御も非常に明示的である。
ADO(ActiveX Data Objects)はより抽象化されており、接続文字列やカーソルの種類によって挙動が複雑に変わるが、DAOは「Workspace」という概念を通じて、よりダイレクトにデータの整合性を支配できる。
2. プロダクション品質のトランザクション・テンプレート
以下に、私が設計現場で「これ以外は認めない」と指示している、DAOによるトランザクション処理の標準テンプレートを示す。
このコードは、単なるエラー回避ではない。オブジェクトのライフサイクル管理、エラー情報の保持、そして確実に状態を巻き戻すための防御的設計が組み込まれている。
'' ======================================================================================
'' 概要: 業務処理の原子性を担保するトランザクション実行テンプレート
'' 設計思想:
'' 1. Workspace単位でのトランザクション管理
'' 2. エラー発生時の確実なRollbackと、エラー情報の伝搬
'' 3. オブジェクトの明示的な解放によるロックの防止
'' ======================================================================================
Public Function ExecuteBusinessProcess() As Boolean
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim rsStock As DAO.Recordset
Dim rsSales As DAO.Recordset
Dim errNum As Long
Dim errDesc As String
' 初期化
On Error GoTo Err_Handler
Set ws = DBEngine.Workspaces(0)
Set db = CurrentDb
' --- [Step 1: トランザクションの開始] ---
' ここからCommitされるまで、全ての変更は「仮の状態」として保持される
ws.BeginTrans
' --- [Step 2: 業務ロジックの実行] ---
' 例: 在庫の更新
Set rsStock = db.OpenRecordset("SELECT StockQty FROM T_Inventory WHERE ItemID = 1", dbOpenDynaset)
If Not rsStock.EOF Then
rsStock.Edit
rsStock.Fields("StockQty").Value = rsStock.Fields("StockQty").Value - 1
rsStock.Update ' ここで失敗しても、BeginTransのおかげでDBには反映されない
End If
rsStock.Close
Set rsStock = Nothing
' 例: 売上データの追加
Set rsSales = db.OpenRecordset("T_Sales", dbOpenDynaset)
rsSales.AddNew
rsSales.Fields("ItemID").Value = 1
rsSales.Fields("SaleDate").Value = Now()
rsSales.Update ' ここで制約違反等が発生した場合、直前の在庫更新も無効化される
rsSales.Close
Set rsSales = Nothing
' --- [Step 3: 全て成功した場合の確定] ---
ws.CommitTrans
Debug.Print "Transaction Committed Successfully."
ExecuteBusinessProcess = True
Exit_Handler:
' --- [Step 4: クリーンアップ] ---
' エラーの有無に関わらず、オブジェクトを解放してロックを解除する
On Error Resume Next
If Not rsStock Is Nothing Then rsStock.Close: Set rsStock = Nothing
If Not rsSales Is Nothing Then rsSales.Close: Set rsSales = Nothing
Set db = Nothing
Set ws = Nothing
Exit Function
Err_Handler:
' --- [Step 5: エラー発生時のロールバック] ---
errNum = Err.Number
errDesc = Err.Description
' トランザクション中のエラーであれば、即座にロールバックを実行
' これにより、中途半端な更新(在庫減のみ等)を完全に破棄する
If errNum <> 0 Then
ws.Rollback
Debug.Print "Transaction Rolled Back. Error: " & errNum & " - " & errDesc
End If
' 呼び出し元にエラーを伝えるため、プロパティに格納するか、再送出する
' ここではシンプルにFalseを返し、ログ出力を行う設計とする
ExecuteBusinessProcess = False
Resume Exit_Handler
End Function
3. アーキテクトの視点:なぜこの書き方なのか
このコードには、現場でトラブルを招く「罠」を回避するための意図が込められている。
① `Workspace` オブジェクトの重要性
`db.BeginTrans` というメソッドは存在しない。トランザクションは `Database` オブジェクトではなく、その上位概念である `Workspace` が管理するものだ。`DBEngine.Workspaces(0)` を明示的に使用することで、現在開いているエンジン全体の整合性を制御している。
② `Exit_Handler` による二段構えの終了処理
VBAにおける最大の落とし穴は、エラー時に `Err_Handler` へ飛んだ後、オブジェクトの解放(`Set rs = Nothing`)を忘れることだ。
レコードセットを閉じずに終了すると、データベースファイルにロック(.laccdb)が残り続け、他のユーザーがデータを更新できなくなるという二次災害を引き起こす。
`Exit_Handler` を設け、エラー時も正常時も必ずクリーンアップを通る構造にすることが、堅牢な設計の鉄則だ。
③ `On Error Resume Next` の限定的な使用
クリーンアップ・セクション(`Exit_Handler`)においてのみ `On Error Resume Next` を使用している。これは、オブジェクトが既に `Nothing` である場合に `Close` メソッドを呼ぶと、それ自体がエラーを吐いてエラーハンドラを混乱させるのを防ぐためだ。エラー処理の中でエラーを出すことは、デバッグにおける悪夢である。
4. 実務への適用:さらなる高みへ
このテンプレートを使いこなすには、以下のルールを自分自身に課してほしい。
1. 「一連の作業」を意識せよ: 複数の `Update` や `Delete` を書くなら、それは一つの `BeginTrans` ~ `CommitTrans` の中に封じ込めなければならない。
2. エラーメッセージを軽視するな: ロールバックが発生した際、単に「エラーです」と出すのではなく、`Err.Number` と `Err.Description` をログに記録せよ。それが、深夜のトラブル対応を救う唯一の武器になる。
3. ネットワーク断絶を想定せよ: 分割データベースを使用している場合、`Rollback` 自体がネットワークエラーで失敗する可能性がある。その場合、データベースエンジンが自動的にタイムアウトによるロールバックを行うが、開発者としては「最悪、手動で整合性を確認する必要がある」というリスク認識を持っておくべきだ。
結論
プロフェッショナルなエンジニアとは、コードが動くことを保証する者ではない。コードが失敗した時に、システムが壊れないことを保証する者である。
DAOのトランザクションとロールバックをマスターすることは、単なる技術習得ではない。それは、ユーザーの資産である「データ」に対する、エンジニアとしての誠実さの証明なのだ。
