Access VBAの壁を越える:ADOによる「一括更新」と、プロが実践する「競合解決」の極意
こんにちは。ここまでAccess VBAの基本を積み上げてきたあなたは、もう「マクロの記録」や「簡単なデータの読み書き」の段階は卒業しています。
しかし、実務で「ユーザーが10人、20人と増えたシステム」を運用し始めると、必ずと言っていいほど直面する「恐ろしい壁」があります。それが、「同時実行制御(コンカレンシー)」の問題です。
「Aさんがデータを書き換えている最中に、Bさんが同じデータを書き換えてしまったらどうなるのか?」
この問題に、単なる「エラーで止まる」のではなく、プロフェッショナルとして「スマートに解決」するための手法を、今日は伝授しましょう。DAOから一歩踏み出し、ADO(ActiveX Data Objects)の真の力を引き出す準備はできましたか?
ここをクリアすれば、あなたのAccess開発は「個人用ツール」から「業務基幹システム」へと昇華しますよ。
1. なぜDAOではなく、ADOを使うのか?
Access開発の標準はDAO(Data Access Objects)です。Accessのテーブルを直接操作するなら、DAOは非常に高速で使いやすい。
しかし、今回扱うような「サーバー(SQL Serverなど)との連携」や「高度な一括処理」、そして「メモリ上だけでデータを保持して、最後にまとめて投げる」といった高度なアーキテクチャを構築する場合、ADOの出番となります。
DAOとADOの決定的な違い
| 特徴 | DAO | ADO |
|---|---|---|
| 主な用途 | Access内部のテーブル操作 | 外部DB連携・高度な一括処理 |
| 思想 | 「今あるテーブル」を操作する | 「データという概念」を操作する |
| メモリ管理 | テーブルに密結合 | Disconnected Recordset(切断型)が可能 |
| 一括更新 | 1件ずつの更新が基本 | BatchUpdate による高速な一括送信が可能 |
今回のテーマである「BatchUpdate」は、ADOが得意とする「メモリ上で編集を溜め込み、最後にドカンと一括で送る」手法です。これにより、ネットワークの往復回数を減らし、パフォーマンスを劇的に向上させることができます。
2. 「楽観的同時実行制御」という考え方
プロの現場では、データを更新する際、常に「誰かが先に書き換えていないか?」を疑います。これを「楽観的同時実行制御(Optimistic Concurrency Control)」と呼びます。
1. 読み取り: ユーザーがレコードを読み取る(この時、データに「バージョン」や「更新日時」を持たせておくとより確実です)。
2. 編集: ユーザーが画面上でデータを書き換える(この間、DBには何も送らない)。
3. 更新試行: 「さあ、更新するぞ!」と命令を送る。
4. 競合検知: もし、自分が読み取った後に、他の誰かがデータを更新していたら、DBは「おっと、データが変わっているよ!」とエラーを返します。
この「エラーをどう受け止め、どう解決するか」が、エンジニアの腕の見せ所です。
3. 実践:Commandオブジェクトによる一括更新と競合解決
それでは、具体的なコードを見ていきましょう。
シナリオはこうです。「在庫管理システムで、複数の在庫データをメモリ上で一括修正し、最後に一括更新(BatchUpdate)を試みる。もし誰かが先に更新していたら、エラーを検知してユーザーに判断を仰ぐ」。
実装コード例
Option Explicit
' 参照設定: Microsoft ActiveX Data Objects x.x Library を有効にしてください
Public Sub ExecuteBatchUpdateWithConflictResolution()
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim cmd As ADODB.Command
Dim i As Integer
' 接続文字列(環境に合わせて書き換えてください)
Dim strConn As String
strConn = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & CurrentProject.Path & "\YourDatabase.accdb;"
On Error GoTo ErrorHandler
' 1. 接続の確立
Set cn = New ADODB.Connection
cn.Open strConn
' 2. Recordsetの生成(Optimistic Lock を指定するのがポイント)
' adLockOptimistic: 変更時に競合をチェックする設定
Set rs = New ADODB.Recordset
rs.Open "SELECT ID, ItemName, StockQty FROM Inventory", cn, adOpenStatic, adLockOptimistic
' --- ここからメモリ上での編集シミュレーション ---
' 本来は画面上のグリッドなどから値を取得しますが、ここではコードで直接操作します
If Not rs.EOF Then
rs.MoveFirst
' 1件目のデータを変更
rs!StockQty = rs!StockQty - 10
rs.Update ' メモリ上のRecordsetに対して「変更完了」を通知
' 2件目のデータを変更(もしあれば)
rs.MoveNext
If Not rs.EOF Then
rs!StockQty = rs!StockQty + 5
rs.Update
End If
End If
' --- 編集終了 ---
' 3. 一括更新の実行
Debug.Print "一括更新を開始します..."
rs.UpdateBatch ' ここで初めてサーバーへ一括送信される
MsgBox "すべての更新が正常に完了しました。", vbInformation
CleanUp:
If Not rs Is Nothing Then
If rs.State = adStateOpen Then rs.Close
End If
If Not cn Is Nothing Then
If cn.State = adStateOpen Then cn.Close
End If
Set rs = Nothing
Set cn = Nothing
Exit Sub
ErrorHandler:
' 4. 競合エラー(adErrBatchUpdate)のハンドリング
' エラー番号 3021 や、ADO特有の BatchUpdate エラーを判定
If Err.Number = 3706 Or Err.Number = 3021 Then ' 状況に応じてエラー番号を調整
Debug.Print "競合が発生しました。エラー解析を開始します。"
HandleBatchErrors rs
Resume CleanUp
Else
MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical
Resume CleanUp
End If
End Sub
''' <summary>
''' BatchUpdate中に発生したエラーを個別に解析し、ユーザーに解決を促す
''' </summary>
Private Sub HandleBatchErrors(ByRef rs As ADODB.Recordset)
Dim errIdx As Integer
Dim targetID As Long
Dim serverValue As Variant
Dim userValue As Variant
' rs.Errors コレクションには、失敗したレコードの情報が格納されている
For errIdx = 0 To rs.Errors.Count - 1
Debug.Print "エラー " & errIdx & ": " & rs.Errors(errIdx).Description
' エラーが発生したレコードの特定(エラーオブジェクトからRecordsetを参照可能)
' ※ADOのバージョンやプロバイダにより取得方法が異なる場合があります
' ここでは概念的な解決フローを示します
' 1. どのデータが原因か特定する
' 2. 現在のサーバー上の値を取得する
' 3. ユーザーに「サーバーの値」と「自分の値」を提示して選択させる
' --- 実務的な解決フローのイメージ ---
' MsgBox "以下のデータで競合が発生しました。" & vbCrLf & _
' "ID: " & targetID & vbCrLf & _
' "現在のサーバーの値: " & serverValue & vbCrLf & _
' "あなたが入力した値: " & userValue & vbCrLf & _
' "どちらを優先しますか?"
' 選択結果に基づき、rsの値を上書きして再試行するか、その行をスキップするかを決定する
Debug.Print "競合解決フローを起動(UI実装が必要)"
Next errIdx
MsgBox "競合が発生したため、手動での確認が必要です。ログを確認してください。", vbExclamation
End Sub
4. アーキテクトの視点:陥りやすい落とし穴
この実装を現場に投入する際、以下の3点に注意してください。これを知っているだけで、トラブルシューティングの時間は半分以下になります。
① エラーオブジェクトの「深さ」を理解する
`rs.UpdateBatch` が失敗したとき、エラーは単一の `Err` オブジェクトだけでなく、`Recordset.Errors` コレクションの中に格納されます。
「なぜかエラーが出ているのに、どのレコードが原因かわからない!」という事態を防ぐには、必ず `For i = 0 To rs.Errors.Count – 1` のループで、エラーの発生源を特定する癖をつけてください。
② 接続のライフサイクル(Connection Lifecycle)
`Command` オブジェクトや `Recordset` を使う際、親となる `Connection` を先に閉じてしまうと、オブジェクトは迷子になり、メモリリークや予期せぬクラッシュの原因になります。
「一番最後に Connection を閉じる」。これは鉄則です。
③ ユーザーインターフェース(UI)の設計
競合が発生したとき、単に「エラーです」と出すのは二流です。
「他のユーザーが既に値を `100` に変更しています。あなたの変更値 `120` を適用しますか? それともサーバーの値を採用しますか?」
このように、「現在の状態」と「自分の意図」を比較させるUIを設計することこそが、真の業務システムエンジニアの仕事です。
まとめ:次の一歩へ
いかがでしたでしょうか。
ADOの `Command` オブジェクトや `BatchUpdate` は、確かに最初は難解に見えます。しかし、これらを使いこなすことで、Accessは「単なる表計算ソフトの延長」から「堅牢なデータベースアプリケーション」へと進化します。
1. DAOからADOへ:より高度な制御のために。
2. BatchUpdate:パフォーマンスのために。
3. エラーコレクションの走査:正確な競合解決のために。
この3点を意識して、ぜひ自分のプロジェクトに組み込んでみてください。
ここをクリアすれば、あなたはもう、Access VBAの「中級者」を超え、「プロフェッショナル」の入り口に立っていますよ。
応援しています。次回の講義でお会いしましょう!
