【実務・中級編】【上級】CurrentDb.Executeの「dbSeeChanges」オプション:SQL Server連携時の排他制御エラーを解決する – Access VBA解析バイブル

スポンサーリンク

【上級】CurrentDb.Executeの「dbSeeChanges」オプション:SQL Server連携時の排他制御エラーを解決する

開発現場でよくある悪夢について話そう。

Accessをフロントエンド、SQL Serverをバックエンド(いわゆるA/S構成)にした大型案件で、モダナイゼーションやアップサイジングを無事に終え、「さあ、高速化されたシステムを納品だ」と意気込んだ矢先、ユーザーからの第一声がこう届く。

> 「なんか、データを更新しようとすると『他のユーザーが変更しました。保存しますか?』っていうエラーが出て業務が止まるんだけど」

開発機(ローカル)のテストでは一度も起きたことがない。焦ってコードを見直すが、SQLの構文に間違いはない。原因はSQL Server特有の「オプティミスティック同時実行制御(楽観的排他制御)」と、Access VBAのデフォルト挙動のミスマッチにある。

今回は、この厄介なエラーのメカニズムを解体し、プロダクション環境で絶対に破綻しない`dbSeeChanges`の正しい実装パターンを、チーフアーキテクトの私から授けよう。

1. なぜ「あのエラー」は起きるのか?(根本原因の解体)

まず、Accessの基本であるJet/ACEエンジンと、SQL Serverの挙動の違いを理解しなければならない。

Access単体のデータベース(`.accdb`)であれば、レコードの更新時に競合が発生しても、内部エンジンがよしなに調停してくれるか、あるいは単純な行ロックで処理される。しかし、バックエンドがSQL Serverの場合、ODBC経由でデータがやり取りされる。

SQL Serverには、テーブルに「タイムスタンプ(行バージョン:`rowversion`)列」が存在する場合がある。レコードが更新されるたびにこの値が自動的にインクリメントされ、「自分が読み込んだ時点から、他の誰かがこの行を書き換えていないか?」を厳密に監視する仕組みになっているのだ。

ここで、VBAから `CurrentDb.Execute` や `DoCmd.RunSQL` を使ってデータを更新・追加したとき、Access側が「裏で何が起きているか」を検知できないままコミットしようとすると、SQL Serverはデータの整合性を守るために例外を吐き出す。これが、あの悪名高い実行時エラー(「他のユーザーによって変更されました」)の正体である。

2. 避けるべき「アンチパターン」

現場の初中級プログラマーがやりがちな、最も効率の悪いアプローチを見てみよう。

‘ 【絶対にしてはいけないアンチパターン】
Sub UpdateUserStatus_Bad(ByVal userId As Long)
Dim sql As String
sql = “UPDATE T_Users SET Status = ‘Active’ WHERE ID = ” & userId

‘ エラーが出るからといって、安易にエラーを握りつぶす
On Error Resume Next
CurrentDb.Execute sql, dbFailOnError
If Err.Number <> 0 then
‘ エラーログも出さずにスルー(データの整合性が死ぬ)
MsgBox “更新できませんでした”, vbCritical
End If
On Error GoTo 0
End Sub

なぜ、この書き方は非効率かつ危険なのか?
エラーを `On Error Resume Next` で隠蔽するのは、エンジニアとしての怠慢だ。データが更新されているのか、競合してロストしたのかすら分からない「サイレントバグ」を生み出し、後からデータ整合性の調査で何十時間もドブに捨てることになる。

SQL Server連携において、`CurrentDb.Execute` を使う場合は、明示的に「変更を監視し、必要なら競合を検知・処理する」オプションを渡さなければならない。それが `dbSeeChanges` である。

3. 正解:`dbSeeChanges` を武装した堅牢なプロダクションコード

ここからが本題だ。実務の現場でそのまま使える、堅牢性と保守性を極限まで高めたデータ更新モジュールの実装例を提示する。

トランザクション制御、エラーハンドリング、そして `dbSeeChanges` の付与を完璧に網羅している。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 模块名: 模듈_データアクセスコントロール
‘ 概要 : SQL Serverバックエンドを想定した安全なレコード更新処理
‘ アーキテクトノート:
‘ auto-increment / rowversionを持つSQL Serverテーブルに対して
‘ Executeメソッドを使用する場合は、必ず dbSeeChanges を付与すること。
‘ =========================================================================
Public Function SafeUpdateUserStatus(ByVal userId As Long, ByVal newStatus As String) As Boolean
Dim db As DAO.Database
Dim sql As String

SafeUpdateUserStatus = False

‘ CurrentDbは毎回呼び出すとオーバーヘッドになるため変数に格納
Set db = CurrentDb()

‘ SQL構築(SQLインジェクション対策としてパラメータクエリを使うべきだが、
‘ 今回は簡略化のため数値とリテラルで記述。実務ではQueryDefを推奨)
sql = “UPDATE T_Users ” & _
“SET Status = ‘” & Replace(newStatus, “‘”, “””) & “‘, ” & _
” UpdatedAt = Now() ” & _
“WHERE ID = ” & userId

‘ トランザクション開始
db.BeginTrans
On Error GoTo ErrorHandler

‘ 【極めて重要】
‘ dbSeeChanges: 自動番号型やタイムスタンプ型を持つSQL Serverテーブルの
‘ 競合を正しく検知・制御するために必須のオプション。
‘ dbFailOnError: 途中でエラーが発生した場合に即座にロールバックさせる。
db.Execute sql, dbSeeChanges + dbFailOnError

‘ コミット
db.CommitTrans
SafeUpdateUserStatus = True

GoTo Finally

ErrorHandler:
‘ 競合エラー(SQL ServerからのODBC-1番号等)をトラップ
Dim errerItem As DAO.Error
Select Case Err.Number
Case 3197 ‘ 「別のユーザーがデータを変更しました」等のDAOエラー番号
MsgBox “他のユーザーがこのデータを同時に更新しています。” & vbCrLf & _
“最新のデータを取得し直してから再度やり直してください。”, vbExclamation, “排他制御エラー”
Case Else
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “システムエラー”
End Select

‘ 異常発生時はロールバック
db.Rollback
SafeUpdateUserStatus = False

Finally:
Set db = Nothing
End Function

4. チーフアーキテクトからの実践的アドバイス

このコードを実装するにあたり、現場のリーダーとして以下の3点をチームに徹底してほしい。

1. `CurrentDb` の乱用を避ける
プロシージャ内で何度も `CurrentDb.Execute` を書くと、その都度内部で新しい接続セッションやオーバーヘッドが発生する。上記のコードのように `Set db = CurrentDb()` として参照を保持し、トランザクションを明示的にコントロールするのがプロの作法だ。
2. インサート(INSERT)時にも `dbSeeChanges` は必要か?
実は、新規追加(INSERT)時であっても、SQL Server側で自動採番される主キー(Identity)の値を直後の処理で取得する必要がある場合や、トリガーが組み込まれているテーブルに対しては、`dbSeeChanges` が必要になるケースが多い。「SQL Serverと連携するテーブルへの書き込みには、原則として `dbSeeChanges` をセットでつける」というコーディング規約をチームで策定してしまうのが最も安全だ。
3. 根本的な解決としての `QueryDef`(パラメータクエリ)
サンプルコードでは文字列結合を用いているが、プロダクション環境では `QueryDef` オブジェクトをあらかじめ作成し、Parametersコレクションに値をバインドして実行する方式をとるべきだ。これにより、SQLインジェクションのリスクをゼロにし、SQL Server側の実行プランのキャッシュ効率も飛躍的に向上する。

5. まとめ

AccessとSQL Serverの連携システムにおいて、エラーハンドリングと排他制御の理解は、そのシステムの寿命を左右する。

「動けばいいや」で `dbSeeChanges` を省いたコードは、ユーザー数が数人から数十人に膨れ上がった瞬間に崩壊を始める。今回解説した仕組みを理解し、正しいオプション選択とトランザクション設計を導入することで、スケーラブルで強靭なエンタープライズAccessアプリケーションを構築してほしい。

あなたの書くコードが、プロジェクト全体の信頼性を支える盾となることを期待している。

タイトルとURLをコピーしました