【テクニカル・上級編】ADOでSQL ServerのIDENTITY列を取得する:新規レコード追加直後のID取得術 – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

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環境を、堅牢なエンタープライズ・アーキテクチャへと昇華させる唯一の道である。

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