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