Access VBAを掌握する極限の知見:SQL Server連携における `dbSeeChanges` の呪縛とエンジニアリング的解法
長年、数多のレガシーシステムとモダンなデータベースの狭間で、Access VBAの限界領域を突破し続けてきたエンジニア諸君。
「ADP(Accessプロジェクト)の廃止」、そして「ODBC/ACDBE(Accessデータベースエンジン)によるSQL Serverへの接続」というモダン化の波の中で、君たちは幾度となくあの冷徹なエラーメッセージに直面してきたはずだ。
> 「実行時エラー ‘3197’: データの変更中に、別のユーザーまたは別のプロセスが同じデータを変更しました。」
> (または「変更または削除しようとしている行は、他のユーザーによって変更されています…」)
ローカルのJet/ACEエンジンで完結していた時代には忘れていた「競合(Concurrency)」の現実。
この不可避の壁を突破するための鍵が、`CurrentDb.Execute` メソッドにおける `dbSeeChanges` オプションである。
本稿では、単なる「おまじない」としてのフラグ指定を超え、SQL Serverのアーキテクチャ、ODBCドライバの挙動、そしてAccessオブジェクトモデルのライフサイクルまで踏み込んだ、極限の知見を授ける。
—
1. なぜ `dbSeeChanges` が必要なのか:アーキテクチャの根底を理解する
まず、根本的なメカニズムを定義する。
AccessからSQL Server(あるいはAzure SQL Database)へODBC経由で接続し、`CurrentDb.Execute` や `DoCmd.RunSQL` を用いてアクションクエリ(INSERT, UPDATE, DELETE)を実行するとき、何が起きているのか。
ジェットエンジンとSQL Serverの思想の乖離
古いAccess(Jet/ACE)のデフォルトの振る舞いは、ローカル完結型だ。ページレベルのロックやオプティミスティック(楽観的)同時実行制御を前提としており、ローカルのキャッシュとストレージが直結している。
しかし、バックエンドにSQL Serverを据えた瞬間、データベースエンジンは行レベルのロック、トリガーの存在、そして「自動採番(IDENTITY)カラム」や「タイムスタンプ(ROWVERSION)」といった非同期のデータ変更を管理下に置く。
ここで問題になるのが、以下の2点だ。
1. INSERT時のIDENTITY値の取得: 挿入されたレコードの自動採番IDを、ACDBEが正確に把握する必要がある場合。
2. トリガーや既定値による暗黙の変更: SQL Server側で `INSTEAD OF` トリガーや `AFTER` トリガーが走る、あるいはデフォルト制約によって、クライアントが送信したデータとは異なる値に書き換わった場合。
SQL Serverは、データの整合性を担保するため、「クライアントが知らないところでデータが書き換わった可能性があるなら、そのままでは書き込みを許可しない(あるいは競合エラーを吐く)」という厳格なポリシーを持つ。
ここで `dbSeeChanges`(値: `32` または `dbSeeChanges` 定数)を付与しないと、ACDBEは「安全のため」に処理を中断し、エラー 3197 を発生させるのだ。
—
2. 適用範囲と「やってはいけない」アンチパターン
シニアエンジニアとして、私は数々の現場で「とりあえず動くから」と全ての `Execute` に `dbSeeChanges` を貼り付け、あるいは逆に一切使わずに本番障害を引き起こしたコードを見てきた。
`dbSeeChanges` が「必須」となる条件
以下の条件のいずれか、あるいは複数が重なるSQL Server上のテーブルを操作する場合、`dbSeeChanges` なしでの `Execute` はバグを埋め込んでいると同義である。
- テーブルに `IDENTITY`(自動採番) カラムが存在し、その値に依存する処理がある。
- テーブルに `ROWVERSION`(旧 `timestamp`) カラムが存在する。
- サーバー側で トリガー (Trigger) が定義されており、挿入・更新時にデータが書き換わる。
- 複数ユーザーが同時に同一レコードを更新する可能性が高い高負荷なトランザクション。
レコードセット(Recordset)との決定的な違い
勘違いしやすい点だが、`Recordset.Edit` / `Recordset.Update` の世界では、ODBC接続時に自動的にこの競合検知が働き、明示的に `dbSeeChanges` を引数に渡す必要がある(例: `rst.Update` ではなく、レコードセットを開く際のオプションやエンジン内部の挙動に依存するが、DAOの `OpenRecordset` ではオプションとして指定する)。
しかし、`CurrentDb.Execute` は一撃でSQLをサーバーに投げる「非接続型に近い高速な一括処理」であるため、オプションとしての明示が不可欠なのだ。
—
3. 【実践】メモリ最適化とトランザクション制御を極めたVBAコード
では、実際の業務システムで耐えうる、完璧なエラーハンドリングとオブジェクト管理を備えたコードを提示しよう。
ここでは、単なる構文ではなく、以下の「プロの要件」を満たしていることに注目してほしい。
1. `CurrentDb` の変数化によるオーバーヘッドの排除(プロパティアクセスの乱用禁止)
2. トランザクション(`BeginTrans` / `CommitTrans` / `Rollback`)の完全な制御
3. 明示的なオブジェクトの解放とエラー時の確実なロールバック
‘ ==============================================================================
‘ 担当: チーフアーキテクト
‘ 概要: SQL Server連携環境における安全なレコード更新処理(dbSeeChanges適用)
‘ ==============================================================================
Public Sub SafeExecuteWithSQLServer()
Dim wrk As DAO.Workspace
Dim db As DAO.Database
Dim strSQL As String
Dim lngAffected As Long
‘ 1. ワークスペースの取得(トランザクション制御用)
Set wrk = DBEngine.Workspaces(0)
‘ 2. CurrentDbを変数に格納(毎回CurrentDbを呼ぶとCOMのオーバーヘッドとインスタンス乱立が発生する)
Set db = CurrentDb()
On Error GoTo ErrorHandler
‘ トランザクションの開始
wrk.BeginTrans
‘ 3. 実行するSQLの構築
‘ ※IDENTITYカラムを持つテーブルへのINSERT、またはトリガーが存在するテーブルのUPDATEを想定
strSQL = “INSERT INTO dbo.T_OrderHistory (CustomerID, OrderDate, StatusMemo) ” & _
“VALUES (10042, SYSDATETIME(), ‘自動連携バッチによる登録’);”
‘ 4. Executeの実行 (dbSeeChangesを必ず付与)
‘ ※dbFailOnErrorを同時に指定し、SQLの一部エラーでサイレント失敗するのを防ぐ
db.Execute strSQL, dbSeeChanges + dbFailOnError
‘ 影響を受けたレコード数の確認(必要に応じたログ出力など)
lngAffected = db.RecordsAffected
Debug.Print “影響行数: ” & lngAffected
‘ トランザクションのコミット
wrk.CommitTrans
MsgBox “データベースへの書き込みが正常に完了しました。”, vbInformation, “処理成功”
CleanUp:
‘ 5. オブジェクトの明示的解放(メモリリークの完全防止)
On Error Resume Next
Set db = Nothing
Set wrk = Nothing
Exit Sub
ErrorHandler:
‘ 異常発生時のロールバック
wrk.Rollback
‘ エラーログの出力と詳細なハンドリング
MsgBox “致命的なエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“詳細: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
—
4. チーフアーキテクトからの警鐘:レガシーとモダンの境界線で
Access VBAは、その手軽さゆえに「誰でも動くものが作れる」という魔力を持っている。しかし、バックエンドに企業の中核システムであるSQL Serverを据えた瞬間、それはもはや「ローカルおもちゃデータベース」ではない。
ネットワークの遅延、コネクションプーリングの枯渇、そして今回解説した「データ競合(Concurrencies)」という、エンタープライズ特有の牙を剥き出しにする。
`dbSeeChanges` を忘れるということは、時限爆弾を抱えて高速道路を逆走するようなものだ。
単にエラーが出たからおまじないをつけるのではなく、「なぜデータベースエンジン間でロックと変更検知の差異が生じるのか」という原理原則を腹に落とし込めてこそ、真に信頼されるシステムを構築できる。
コードを書くときは常に想像せよ。今、この瞬間に何十人ものユーザーが同じデータベースを叩いているかもしれないという現実を。
妥協なき設計とコードだけが、システムを、そしてエンジニア自身の身を救うのだ。
