Access×SQL Server連携の致命的罠:なぜ「dbSeeChanges」なしのExecuteはプロ失格なのか
Accessをフロントエンドとし、バックエンドにSQL Server(Azure SQL Database含む)を据えるハイブリッド構成は、社内業務システムのスケールアップにおける王道パターンだ。
しかし、ローカルのMDB/ACCDBの感覚のままVBAを書き、SQL Serverのリンクテーブルに対して`CurrentDb.Execute`を実行した瞬間、君は不可解なエラーの迷宮に放り込まれることになる。
「実行時エラー ‘3146’: ODBC — 呼び出しが失敗しました。」
この、中身が全く見えない不親切なエラーの裏に潜む真犯人こそが、今回解説する`dbSeeChanges`オプションの欠落であり、Accessと外部RDB間の通信仕様に対する理解不足である。
今回は、このエラーが発生する根本原因から、コーディングにおけるパフォーマンスの極意、そしてそのまま本番環境に投入できる堅牢なプロダクションコードまでを徹底的に解説する。
—
1. なぜ `dbSeeChanges` が必要なのか?
結論から言おう。
SQL Server側で「IDENTITY列(自動連番)」が定義されているテーブルに対し、DAO経由でデータを挿入・更新・削除する場合、`dbSeeChanges` オプションの指定は「絶対義務」である。
内部で何が起きているのか?
Accessのデータアクセスエンジン(ACE/Jet)は、SQL Serverのリンクテーブルに対してデータを書き込んだ直後、「自分が今書き込んだレコードが、他者によって変更されていないか、あるいは今挿入したIDENTITY値(主キー)は何か」を確認しにいく仕様になっている。
このとき、`dbSeeChanges` オプションが指定されていないと、ACEエンジンは「他ユーザーによる同時変更(See Changes)を追跡する準備ができていない」と判断し、安全のために処理を強制中断する。これがエラー3146の正体だ。
「自分一人しか使っていない開発環境だから大丈夫」という言い訳は通用しない。SQL Server側にIDENTITY列が存在する時点で、アクセス人数に関係なくエンジンレベルでエラーを吐く。
—
2. アーキテクチャの罠:`CurrentDb` をそのまま使うな
コードの書き方を解説する前に、もう一つ、多くのAccess開発者が犯しているパフォーマンス上の大罪を指摘しておかなければならない。
‘ 【アンチパターン】これを目にするたびに私は目眩がする
For i = 1 To 1000
CurrentDb.Execute “INSERT INTO …”, dbFailOnError + dbSeeChanges
Next i
何が悪いか分かるだろうか?
`CurrentDb` は単なるプロパティではない。呼び出されるたびに、現在のデータベースオブジェクトを裏で再作成(インスタンス化)し、リンクテーブルの情報を再ロードする関数なのだ。
ループの中で `CurrentDb` を連発すると、システムはメモリの確保と解放、テーブル情報の再読込を数千回繰り返すことになり、著しいパフォーマンス低下(時にはフリーズ)を招く。
正しいアプローチ
データベースオブジェクトは必ず一度だけ変数にキャプチャするか、`DBEngine(0)(0)` を使用して既存のインスタンスを使い回すのが鉄則だ。
‘ 【推奨】オブジェクトのライフサイクルをコントロールする
Dim db As DAO.Database
Set db = CurrentDb ‘ インスタンス化は1回だけ
For i = 1 To 1000
db.Execute “INSERT INTO …”, dbFailOnError + dbSeeChanges
Next i
Set db = Nothing ‘ 確実に解放する
—
3. 実戦投入用:堅牢極まるプロダクションコード
プロのコードには、単に「動く」だけでなく、「例外が発生したときに原因を自己主張する能力」が求められる。
前述の通り、ODBCエラー(3146)が発生した際、VBAの通常の `Err.Description` は「ODBC — 呼び出しが失敗しました。」としか教えてくれない。これでは、SQL Server側でトリガーが失敗したのか、制約違反(FOREIGN KEY違反など)が起きたのかが全く分からない。
以下のコードは、`DBEngine.Errors` コレクションを解析し、SQL Serverが返した真のエラーメッセージを暴き出すエラーハンドラーを搭載した、極めて堅牢な関数テンプレートである。
”’
”’
”’ 実行するSQL文字列
”’
Public Function ExecuteSecureSQL(ByVal sql As String) As Boolean
On Error GoTo Err_Handler
Dim db As DAO.Database
Set db = CurrentDb
‘ dbFailOnError: エラー発生時にロールバックを強制する
‘ dbSeeChanges: SQL ServerのIDENTITY列操作に伴う競合監視を有効化する(必須)
db.Execute sql, dbFailOnError + dbSeeChanges
‘ 実行が成功した場合
ExecuteSecureSQL = True
Exit_Handler:
‘ オブジェクトのクリーンアップ(メモリリーク防止)
On Error Resume Next
If Not db Is Nothing Then Set db = Nothing
Exit Function
Err_Handler:
ExecuteSecureSQL = False
‘ ODBCエラーの詳細を暴くための特殊処理
If Err.Number = 3146 Then
Dim daoErr As DAO.Error
Dim errorDetail As String
errorDetail = “【ODBC詳細エラーレポート】” & vbCrLf
‘ DBEngineに蓄積されたすべての下位エラー(SQL Serverからの生の声)をループ処理
For Each daoErr In DBEngine.Errors
errorDetail = errorDetail & “Error No: ” & daoErr.Number & vbCrLf & _
“Source: ” & daoErr.Source & vbCrLf & _
“Description: ” & daoErr.Description & vbCrLf & _
“———————————–” & vbCrLf
Next daoErr
‘ 開発者やシステム管理者が一目で原因を特定できるようにログ出力(またはメッセージ表示)
Debug.Print errorDetail
MsgBox “SQL Serverとの通信中にエラーが発生しました。詳細は以下の通りです:” & vbCrLf & vbCrLf & errorDetail, vbCritical, “データベースエラー”
Else
‘ 一般的なVBAエラーの処理
MsgBox “システムエラーが発生しました:” & vbCrLf & Err.Description, vbCritical, “エラー No: ” & Err.Number
End If
Resume Exit_Handler
End Function
—
4. SQL Server連携における「プロの防衛策」
`dbSeeChanges` を付与する以外にも、Access + SQL Server 連携を成功させるために絶対に外してはならないアーキテクチャ設計基準が2つある。これらを怠ると、システムは簡単に「データの競合」や「更新不可能」といったバグを引き起こす。
① すべてのテーブルに「主キー(Primary Key)」を設置せよ
Accessは、リンクテーブルに主キーが設定されていない場合、そのテーブルを完全な読み取り専用(ReadOnly)として認識する。
SQL Server側で主キーがないテーブルをAccessにリンクすると、リンク時に「一意の識別子の選択」を求められる。ここで指定を怠ると、VBAからのいかなるインサートもアップデートも受け付けなくなる。データベース設計の基本中の基本だが、怠っているプロジェクトが驚くほど多い。
② テーブルには必ず `timestamp`(RowVersion)型列を実装せよ
SQL Serverにおける `timestamp` 型(現在は `rowversion` と呼ばれる)は、時間ではなく「レコードが更新されたバージョン」を記録するバイナリ列だ。
これがないと、Accessはレコードを更新する際、「すべての列の値が、自分が読み込んだときと同じか」を検証する非効率なSQLを発行する。このとき、浮動小数点(float)やNULLの扱いによって、値が変わっていないにもかかわらず「他のユーザーがこのデータを変更しました」という誤検出(競合エラー)が多発する。
テーブルに `timestamp` 型の列を1つ追加しておくだけで、Accessは「この値が変わっていないか」だけをスマートに監視するようになり、競合エラーの発生率は劇的に低下し、パフォーマンスも向上する。
—
5. まとめ:動くコードから、壊れないコードへ
今回の要点をまとめる。
1. SQL Server側にIDENTITY列があるなら、`dbSeeChanges` は絶対必須。
2. `CurrentDb` の多用を避け、オブジェクトのインスタンスを適切に管理する。
3. エラー3146は `DBEngine.Errors` から真の原因を引っ張り出す。
4. SQL Serverのテーブルには「主キー」と「timestamp型」を必ず仕込む。
「とりあえずエラーが出ないから」と放置されたコードは、本番環境のデータ量が増え、マルチユーザーでの同時実行が多発した瞬間に必ず牙をむく。
優れたアーキテクトとは、トラブルが起きてから素早く直す者ではない。最初からトラブルが起き得ない構造をコードに仕込んでおく者だ。君の書く次のコードが、真に堅牢なものであることを期待する。
