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

スポンサーリンク

Access VBAを掌握する極限の知見:CurrentDb.Executeの「dbSeeChanges」が救うSQL Server連携の暗部

レガシーとモダンが交錯する企業内システムの現場において、Microsoft Accessは今なお「最強の超高速プロトタイピング・ツール」であり、同時に「最も誤解されているデトネーター(起爆装置)」でもある。

フロントエンドにAccess、バックエンドにSQL Server(UPS:Up-sizing)を採用したアーキテクチャは、部門系システムにおいて依然として強力な選択肢だ。しかし、この構成をとった瞬間に開発者を襲う、最も悪名高いエラーがある。

> 実行時エラー ‘3197’:
> データは、他のユーザーによって同時に変更されています。

このエラーの本質を理解せず、場当たり的なエラーハンドリングで逃げているうちは、シニアエンジニアを名乗る資格はない。今回は、Access VBAの心臓部である `CurrentDb.Execute` と、SQL Server連携における排他制御の真実を解き明かす。

1. なぜ「他のユーザーが変更しました」と言われるのか?

Jet/ACEエンジン(Accessのデータベースエンジン)と、企業向けRDBであるSQL Serverの間には、並行性制御(Concurrency Control)の哲学に決定的な乖離がある。

Accessはデフォルトで「楽観的排他制御(Optimistic Concurrency)」の変種を採用している。テーブルのレコードを更新する際、Accessは「最後に読み込んだ時点から値が変わっていないこと」を確認するために、すべての列(あるいはタイムスタンプ/RowVersion列)をWHERE句の条件に含めてUPDATEを発行する。

ここでSQL Server側の仕様が絡む。
SQL Serverのテーブルに「行バージョン(`rowversion` / 旧 `timestamp` 型)」列が存在する場合、あるいはトリガーやIDENTITY(自動採番)カラムが絡む更新が行われた場合、Jetエンジンがその変更をリアルタイムに検知できない、あるいはODBC経由のトラッキングにおいて不整合が生じる。結果として、「自分自身が書き込んだ瞬間、あるいは他セッションの非同期更新と競合した瞬間」に、あの忌まわしいエラー3197がスローされる。

特に、`DoCmd.RunSQL` や `CurrentDb.Execute` を用いて、バックエンドのSQL Serverテーブルに対して一括処理やトランザクション内の更新を行った際、この現象は高頻度で顕在化する。

2. 唯一の処方箋:`dbSeeChanges` という名の免罪符

この問題を根本から解決するための鍵が、`CurrentDb.Execute` メソッドに用意されたオプション定数 `dbSeeChanges` (値: 128) である。

公式ドキュメントには「自動採番フィールドを使用しているSQL Serverテーブルを変更する場合に指定する」とサラリと書かれているが、その実態は「ODBCリンクテーブルに対する楽観的排他制御の競合チェックを適切に制御し、最新の行バージョンを強制的に追従させるためのフラグ」である。

誤った実装(レガシーな悪習)

‘ 【アンチパターン】エラー3197の温床となるコード
Sub UpdateCustomer_Bad(ByVal customerID As Long, ByVal newName As String)
Dim sql As String
sql = “UPDATE dbo_Customers SET CompanyName = ‘” & newName & “‘ WHERE ID = ” & customerID

‘ エラー対策をしていないため、SQL Server連携時に高確率で爆発する
CurrentDb.Execute sql, dbFailOnError
End Sub

このコードは、単体のテストでは動くように見えても、複数ユーザーが同時にアクセスする本番環境や、トリガーが組まれたSQL Serverのテーブルに対して実行した途端に崩壊する。

正しい実装(チーフアーキテクトの推奨パターン)

‘ 【推奨パターン】dbSeeChangesと厳密なオブジェクトライフサイクル管理
Sub UpdateCustomer_Pro(ByVal customerID As Long, ByVal newName As String)
Dim db As DAO.Database
Dim sql As String

On Error GoTo ErrorHandler

‘ CurrentDbは毎回生成コストがかかるため、ローカル変数に参照を保持する
Set db = CurrentDb

sql = “UPDATE dbo_Customers SET CompanyName = ‘” & EscapeSql(newName) & “‘ WHERE ID = ” & customerID

‘ dbSeeChangesを付与し、SQL Serverの行変更検知メカニズムと調停させる
‘ さらに dbFailOnError を併用し、トランザクションの整合性を担保する
db.Execute sql, dbSeeChanges + dbFailOnError

CleanUp:
‘ 【重要】DAOオブジェクトの明示的解放
‘ GCに頼るな。メモリリークとロックの残留を防ぐため、即座に解放する。
If Not db Is Nothing Then Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “SQL Serverとの同期中に排他制御エラーが発生しました。” & vbCrLf & _
“Error No: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “排他制御例外”
Resume CleanUp
End Sub

Private Function EscapeSql(ByVal val As String) As String
‘ 簡易的なSQLインジェクション対策(本来はパラメータクエリを推奨)
EscapeSql = Replace(val, “‘”, “””)
End Function

3. コードレビュー:アーキテクチャの観点からの深掘り

上記のコード片には、プロフェッショナルであれば見逃さない「極限の知見」がいくつか盛り込まれている。

① `CurrentDb` のキャッシュとメモリ管理

`CurrentDb` プロパティは、呼び出されるたびに内部で新しい `DAO.Database` オブジェクトを生成し、システム資源を消費する。ループ内で `CurrentDb.Execute` を叩く愚行は、あっという間にメモリリークとハンドルの枯渇を招く。
必ずローカル変数 `Dim db As DAO.Database` に受け、処理が終わったら `Set db = Nothing` で即座に解放する。これがAccess VBAにおけるメモリ最適化の鉄則だ。

② ビット演算によるオプションの結合

`dbSeeChanges + dbFailOnError` という記述は、DAOの内部定数(ビットフラグ)の足し算である。

  • `dbSeeChanges` (128)
  • `dbFailOnError` (16)

これらを組み合わせることで、「SQL Serverの変更検知を有効にしつつ、クエリの一部が失敗した場合にはロールバックする」という堅牢な実行コンテキストが完成する。

③ なぜパラメータクエリではなく `Execute` なのか?

SQL Server連携において、ODBC経由のパラメータクエリ(`DAO.QueryDef`)は強力だが、複雑なバッチ処理や動的SQLの構築においては、ストアドプロシージャを呼び出すか、直書きの `Execute` がパフォーマンス上有利な場面が存在する。ただし、SQLインジェクションの脆弱性を排除するため、外部入力値は必ずサニタイジング(または `QueryDef` によるパラメータ化)を徹底すべきだ。

4. さらに先へ:真のエンタープライズ連携を目指して

`dbSeeChanges` はあくまで「Access側からSQL Serverを叩く際の救急絆創膏」に過ぎない。
システムが大規模化し、同時接続数が50を超えるような環境では、Accessをフロントエンドに据えた直接のテーブル更新自体がアーキテクチャの限界を迎える。

究極的な解決策は、Accessから直接 `UPDATE`を発行するのではなく、SQL Server側にストアドプロシージャ(Stored Procedure)を作成し、それを `DAO.QueryDef` または ADODB.Command 経由で実行することだ。

‘ 【極限のアーキテクチャ】ストアドプロシージャによる安全な実行
Sub ExecuteStoredProcedure(ByVal customerID As Long, ByVal newName As String)
Dim conn As Object
Set conn = CreateObject(“ADODB.Connection”)

‘ SQL Server Native Client または OLEDB/ODBC経由で直接接続
conn.Open “Driver={ODBC Driver 17 for SQL Server};Server=myServerAddress;Database=myDataBase;Trusted_Connection=yes;”

Dim cmd As Object
Set cmd = CreateObject(“ADODB.Command”)
Set cmd.ActiveConnection = conn
cmd.CommandText = “sp_UpdateCustomer”
cmd.CommandType = 4 ‘ adCmdStoredProc

cmd.Parameters.Refresh
cmd.Parameters(“@CustomerID”) = customerID
cmd.Parameters(“@NewName”) = newName

cmd.Execute

conn.Close
Set cmd = Nothing
Set conn = Nothing
End Sub

このレベルの設計に踏み込めば、Accessの持つJet/ACEエンジンの癖(排他制御の脆弱性)を完全にバイパスし、SQL Server側の強力なトランザクションとロックマネージャの恩恵をダイレクトに受けることができる。

結言

「他のユーザーが変更しました」というエラーは、Accessが無能だから起きるのではない。私たちが、AccessとSQL Serverという「異なる思想で作られた二つの巨獣」を繋ぐルール(作法)を無視しているから起きるのだ。

`dbSeeChanges` を制する者は、Access-SQL Server連携の暗部を制す。
レガシーシステムであっても、アーキテクトの知見と手腕次第で、その寿命を延ばし、堅牢なエンタープライズシステムへと昇華させることが可能である。コードの細部に魂を宿せ。妥協した瞬間に、システムは崩壊する。

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