原子性の極意:DAOトランザクションにおける整合性維持と、中途半端な更新を許さないための鉄則
業務システムの設計において、最も忌むべき事態は「システムが止まること」ではない。「システムは動いているが、データが壊れていること」だ。
特に、Access VBAを用いたローカル・ネットワーク共有環境におけるデータ更新処理では、ネットワークの瞬断、ファイル共有の競合、あるいは予期せぬランタイムエラーが常に牙を剥いている。一連の更新処理のうち、3件目まで成功し、4件目でエラーが発生したとき、それまでの3件を「なかったこと」にできるか。この「原子性(Atomicity)」の確保こそが、シニアエンジニアと単なるコーダーを分かつ境界線である。
本稿では、DAO(Data Access Objects)を用いたトランザクション処理における、極限の堅牢性を備えた実装パターンを解説する。
1. アーキテクチャの深淵:なぜADOではなくDAOなのか
Accessのエンジン(ACE/Jet)を直接操作する場合、我々はADO(ActiveX Data Objects)ではなく、DAOを選択すべき局面が多い。
ADOは汎用的なデータアクセス層であり、SQL ServerやOracleといった外部RDBMSとの連携には極めて強力だ。しかし、Accessのローカルファイル(.accdb)に対する操作においては、DAOの方がエンジンとの親和性が高く、トランザクションの制御においても「エンジン内部の動作」に近い、決定論的な挙動を示す。
DAOのトランザクションは、`Workspace`オブジェクトの管理下にある。この`Workspace`が、ファイルシステム上のロック(.laccdb)と、データベースエンジン内の論理的な整合性を結びつける結節点となるのだ。
2. 破綻するコードの共通点:リソースの放置
多くの開発者が陥る罠は、エラーハンドリングを「エラーメッセージを表示して終了する」ためのものと考えている点だ。
' --- 典型的な失敗例 ---
Sub BadUpdate()
On Error GoTo ErrHandler
Dim db As DAO.Database
Set db = CurrentDb
db.Execute "INSERT INTO T_Orders ..." ' 1件目
db.Execute "UPDATE T_Stock ..." ' ここでエラーが発生したとする
MsgBox "完了"
Exit Sub
ErrHandler:
MsgBox Err.Description
End Sub
このコードは、2件目の`UPDATE`でエラーが発生した際、1件目の`INSERT`は既にファイルに書き込まれたまま(あるいはエンジン内に中途半端な状態で保持されたまま)となる。これが「データ不整合」の正体だ。さらに、オブジェクトの解放が不適切だと、Windowsのプロセスレベルでファイルハンドルが掴みっぱなしになり、後続のプロセスがデータベースにアクセスできなくなる「ゾンビ・ロック」を引き起こす。
3. 極限のテンプレート:堅牢なトランザクション・パターン
以下に、私が設計における「定型句」として推奨している、完全なエラーハンドリングとオブジェクトのライフサイクル管理を組み込んだコードを示す。
''' <summary>
''' 複数の更新処理をアトミック(不可分)に実行するためのテンプレート
''' </summary>
Public Sub ExecuteAtomicTransaction()
' 1. オブジェクトの宣言(スコープを最小限にする)
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim rs As DAO.Recordset
' 2. エラーハンドリングのセットアップ
On Error GoTo ErrorHandler
' 3. Workspaceの取得(トランザクションの司令塔)
' CurrentDbではなく、明示的にWorkspaceを経由するのがプロの作法である
Set ws = DBEngine(0)
Set db = CurrentDb
' --- トランザクション開始 ---
' これ以降の操作は、CommitTransが呼ばれるまで物理的な確定が行われない
ws.BeginTrans
Debug.Print "Transaction Started."
' --- 更新処理の開始 ---
' 例:受注テーブルへの挿入
db.Execute "INSERT INTO T_Orders (OrderDate, CustID) VALUES (Date(), 101)", dbFailOnError
' 例:在庫テーブルの更新(ここでエラーが発生するシナリオを想定)
' dbFailOnError を指定することで、SQLエラーをVBAエラーとして捕捉できる
db.Execute "UPDATE T_Stock SET StockQty = StockQty - 1 WHERE ItemID = 999", dbFailOnError
' --- 更新処理の終了 ---
' 全ての処理が成功した場合のみ、ここで物理的な書き込みを確定させる
ws.CommitTrans
Debug.Print "Transaction Committed Successfully."
CleanExit:
' 4. 決定論的なリソース解放(メモリリークとロックの防止)
' エラーの有無に関わらず、必ずオブジェクトをNothingに落とし、ハンドルを解放する
On Error Resume Next ' 解放処理中のエラーは無視する
If Not rs Is Nothing Then Set rs = Nothing
If Not db Is Nothing Then Set db = Nothing
If Not ws Is Nothing Then Set ws = Nothing
Exit Sub
ErrorHandler:
' 5. ロールバック処理
' エラーが発生した瞬間、BeginTransからCommitTransまでの全ての変更を破棄する
If Not ws Is Nothing Then
ws.Rollback
Debug.Print "Transaction Rolled Back due to error: " & Err.Description
End If
' エラーログの記録や、ユーザーへの通知を行う
MsgBox "致命的なエラーが発生したため、処理を中断しロールバックしました。" & vbCrLf & _
"Error: " & Err.Number & " - " & Err.Description, vbCritical, "System Integrity Error"
' 解放処理へジャンプ
Resume CleanExit
End Sub
4. アーキテクトの視点:実装における「重み」の理解
このコードには、単なる文法以上の「意図」が込められている。
`dbFailOnError` の重要性
`db.Execute` を使用する際、`dbFailOnError` オプションを忘れてはならない。これを指定しない場合、SQLの制約違反(一意制約違反など)が発生しても、VBA側にはエラーが伝播せず、静かに失敗する。これは「サイレント・データ破損」を招く最悪のパターンだ。
オブジェクトのライフサイクルとメモリ管理
VBAはガベージコレクション(GC)の挙動が極めて不透明だ。`Set obj = Nothing` を明示的に記述することは、単なるマナーではない。特に `Workspace` や `Database` オブジェクトは、OSレベルのファイルハンドルや共有メモリを占有する。これらを適切に解放しないことは、Windows APIレベルで言えば、リソースハンドルのリークを引き起こしているのと同義である。
Windows APIとファイルシステムの関係
Accessがトランザクションを行っている間、ACEエンジンは `.laccdb` ファイルを通じて、他のプロセスに対して「排他的な権利」を主張する。ロールバックに失敗したままプロセスが終了すると、OSレベルでロックが保持され続け、ユーザーが「ファイルが使用中です」という不可解なエラーに直面することになる。我々の任務は、コードの実行が「どのような経路を通っても(正常でも異常でも)、必ずクリーンな状態(CleanExit)に到達すること」を保証することだ。
結論
Access VBAにおける開発は、レガシーな環境を扱うがゆえに、現代的な抽象化レイヤーに隠された「物理的なリスク」を常に意識しなければならない。
トランザクションは、単なる機能ではない。それは、システムの信頼性を担保するための「契約」である。このテンプレートを血肉とし、不測の事態においてもデータの整合性を死守する、真にプロフェッショナルなシステムを構築してほしい。
