IDENTITYの深淵を穿つ:ADOにおけるSQL Server自動採番IDの確実な奪取術
業務システムの設計において、データの整合性は絶対的な聖域である。特に、Access VBAをフロントエンドとし、SQL Serverをバックエンドに据えた分散環境において、`IDENTITY`列(自動採番列)の取得は、単なる「値の取得」ではない。それは、トランザクションの原子性を保ち、後続の関連レコードへの紐付けを保証するための、極めてクリティカルな儀式である。
多くの開発者が、`SELECT @@IDENTITY` を安易に使い、トリガーによる予期せぬ副作用や、マルチユーザー環境下でのスコープの混同という泥沼に足を踏み入れる。
本稿では、伝説的なアーキテクチャ設計の観点から、ADOを用いたSQL ServerへのINSERT直後のID取得における「正解」を、その理論的背景と共に提示する。
1. アーキテクチャの境界線:DAOとADOの決定的な違い
まず、我々が立脚している足場を整理しておく。Access VBAにおいて、ローカルのACE/Jetエンジンを操作するDAO (Data Access Objects) と、外部リソースへ接続するADO (ActiveX Data Objects) では、その設計思想が根本から異なる。
- DAO: エンジン密結合型。レコードセットの操作はローカルのカーソル管理下にあり、追加直後のID取得は `Me.Recordset.Bookmark` や `Fields(“ID”).Value` を参照するだけで、エンジンが透過的に処理してくれる。
- ADO: ネットワーク越しのプロトコル型。ADOはSQL Serverに対して「命令」を送り、その「結果」を受け取る。SQL Server側で発生した事象(自動採番)は、ネットワークを越えて自動的にクライアントへ伝播するわけではない。
この「ネットワークの隔たり」を理解していない者が、ADOでIDを取得しようとして失敗する。
2. 致命的な誤解:`@@IDENTITY` vs `SCOPE_IDENTITY()`
SQL Serverには、生成されたIDを取得するための関数が複数存在する。ここでの選択ミスは、システムの信頼性を根底から破壊する。
`@@IDENTITY` の罠
`@@IDENTITY` は、「現在のセッション」で最後に生成されたIDを返す。一見正しく見えるが、もし対象のテーブルに「別のテーブルにログを書き込むトリガー」が設定されていた場合、`@@IDENTITY` はトリガーによって生成されたIDを返してしまう。これは致命的なデータ不整合を招く。
`SCOPE_IDENTITY()` の優位性
`SCOPE_IDENTITY()` は、「現在のスコープ(現在のバッチ、ストアドプロシージャ、またはトリガー内)」で最後に生成されたIDを返す。トリガーの影響を受けず、我々が意図したINSERT文の結果のみを確実に捕捉できる。
しかし、プロフェッショナルの選択は、これらよりもさらに一歩先を行く。
3. 極限の最適解:`OUTPUT` 句によるアトミックな取得
真に堅牢なシステムを構築するならば、`INSERT` 文と `ID` 取得を、単一のSQLステートメントとして結合すべきである。SQL Server 2005以降で導入された `OUTPUT` 句を使用すれば、INSERT操作と同時に、生成された値を「結果セット」としてクライアントに返却させることができる。
これにより、「INSERTを実行する」→「SELECTを実行する」という2往復のネットワーク・ラウンドトリップを、1回に集約できる。 パフォーマンスと原子性(Atomicity)の両面において、これに勝る手法は存在しない。
4. 実装:プロフェッショナル・コード・パターン
以下に、ADOを用いた、メモリ管理とエラーハンドリングを極限まで突き詰めた実装例を示す。
''
' @description: SQL Serverへレコードを追加し、生成されたIDENTITY列の値を確実に取得する
' @param targetTable: 挿入対象のテーブル名
' @param sqlInsert: INSERT文(VALUES句を含む。ただし、ID列は含めないこと)
' @returns: 生成されたID (Long型)
' @throws: データベースエラー、または取得失敗時にエラーを発生させる
''
Public Function InsertAndGetIdentity(ByVal targetTable As String, ByVal sqlInsert As String) As Long
Dim conn As ADODB.Connection
Dim cmd As ADODB.Command
Dim rs As ADODB.Recordset
Dim connectionString As String
' 接続文字列は環境に合わせて管理すること(本来は設定ファイルや定数から取得)
connectionString = "Provider=SQLOLEDB;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;Integrated Security=SSPI;"
On Error GoTo ErrorHandler
' 1. オブジェクトの生成
Set conn = New ADODB.Connection
Set cmd = New ADODB.Command
' 2. 接続の確立
conn.Open connectionString
' トランザクションの開始(原子性の確保)
conn.BeginTrans
' 3. SQLの構築:OUTPUT句を用いて、挿入されたIDを即座に取得する
' 例: INSERT INTO Table (Col1) OUTPUT inserted.ID VALUES ('Val1')
' sqlInsertには "INSERT INTO ... VALUES ..." の形式を想定
' ここでは、極めてスマートに OUTPUT 句を注入する
Dim finalSql As String
finalSql = Replace(sqlInsert, "INSERT INTO ", "INSERT INTO " & targetTable & " OUTPUT inserted.ID ")
' ※注意: 引数にテーブル名が含まれている場合は、Replaceのロジックを調整すること
' 実際の実装では、SQLインジェクションを防ぐため、
' 直接文字列結合せず、Parametersコレクションを使用することを強く推奨する。
' 以下は概念を示すための簡易的な構築例。
finalSql = "INSERT INTO " & targetTable & " OUTPUT inserted.ID " & Mid(sqlInsert, InStr(sqlInsert, "VALUES"))
With cmd
.ActiveConnection = conn
.CommandText = finalSql
.CommandType = adCmdText
End With
' 4. 実行と結果の取得
' Executeメソッドは、OUTPUT句の結果をRecordsetとして返す
Set rs = cmd.Execute
If Not rs.EOF Then
InsertAndGetIdentity = rs.Fields(0).Value
Else
Err.Raise vbObjectError + 513, "InsertAndGetIdentity", "IDの取得に失敗しました。"
End If
' 5. コミット
conn.CommitTrans
CleanUp:
' 6. メモリの明示的解放(極めて重要)
' Access VBAにおいて、COMオブジェクトの解放を怠ると、
' 参照カウントが残り、SQL Server側のロックが解放されない等の怪奇現象を引き起こす。
On Error Resume Next
If Not rs Is Nothing Then
If rs.State = adStateOpen Then rs.Close
Set rs = Nothing
End If
If Not cmd Is Nothing Then Set cmd = Nothing
If Not conn Is Nothing Then
If conn.State = adStateOpen Then conn.Close
Set conn = Nothing
End If
Exit Function
ErrorHandler:
' エラー発生時はロールバック
If Not conn Is Nothing Then
If conn.State = adStateOpen Then conn.RollbackTrans
End If
MsgBox "Critical Error: " & Err.Description, vbCritical, "Architecture Violation"
Resume CleanUp
End Function
5. アーキテクトの視点:メモリとライフサイクル
上記のコードで、私が執拗に `Set … = Nothing` を繰り返している理由を理解してほしい。
Access VBAは、COM(Component Object Model)の海の上で動いている。ADOオブジェクトは、単なる変数ではなく、外部プロセス(SQL Server)と通信するための重厚なインターフェースである。
`Set rs = Nothing` を怠るということは、そのインターフェースが「まだ使われているかもしれない」とWindows OSに誤認させることを意味する。これが累積すると、メモリリークを招くだけでなく、「SQL Server側でレコードがロックされたままになり、他のユーザーがタイムアウトする」という、デバッグ困難な分散システム特有の不具合を誘発する。
6. 結論
ADOを用いてSQL ServerからIDを取得する際、我々が目指すべきは「動くコード」ではない。「不可逆的な整合性を保証するコード」である。
1. `@@IDENTITY` を捨て、`SCOPE_IDENTITY()` を理解せよ。
2. さらにその先、`OUTPUT` 句によるアトミックな取得をマスターせよ。
3. トランザクションとオブジェクトのライフサイクルを、神聖な儀式として扱え。
この規律を守ることこそが、レガシーなAccess環境を、堅牢なエンタープライズ・アーキテクチャへと昇華させる唯一の道である。
