【入門編】ADOとSQL Serverの「IDENTITY列」取得:新規レコード追加直後のIDを確実に取得する実装パターン – Access VBA解析バイブル

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

マクロの記録から卒業する。SQL Serverの「IDENTITY列」を確実に、安全に奪取する極限の実装パターン

こんにちは。現場の設計と実装を司るチーフアーキテクトです。

Access VBAを学び始め、マクロの記録から一歩踏み出し、SQL Serverをバックエンドに据えた「本格的な業務システム」の構築に挑戦している皆さん。素晴らしいステップアップです。

しかし、ここで一つの大きな壁にぶつかる人がいます。それは、「データを追加した直後に、自動採番されたID(IDENTITY列)をどうやって取得するか?」という問題です。

「`SELECT MAX(ID) FROM テーブル` で取ればいいじゃないか」
「`@@IDENTITY` を使えばいいんだろ?」

もしあなたがそう考えているなら、少しだけ立ち止まってください。その実装は、ユーザーが同時に操作した瞬間に、全く別の誰かのデータIDを掴んでしまう「致命的なバグ」を孕んでいます。

今日は、プロが現場で使う「絶対に失敗しないID取得の作法」を、ADO(ActiveX Data Objects)のアーキテクチャの観点から徹底的に解説します。ここをクリアすれば、あなたのVBAは「動くコード」から「信頼されるシステム」へと昇華します。

1. なぜ「MAX(ID)」ではいけないのか?(競合の恐怖)

まず、初心者が陥りやすい「危険なパターン」を理解しましょう。

【NGパターン】MAX(ID) を取得する

' ❌ 極めて危険な実装例
DoCmd.RunSQL "INSERT INTO T_Orders (CustomerName) VALUES ('田中太郎')"
newID = DMax("OrderID", "T_Orders") ' ← ここで大問題が発生する

何が問題なのか?
想像してください。あなたのシステムを、10人のユーザーが同時に使っている場面を。

1. ユーザーAがデータを追加する(ID: 100が生成される)。
2. (コンマ数秒の隙に) ユーザーBがデータを追加する(ID: 101が生成される)。
3. ユーザーAのプログラムが `MAX(ID)` を実行する。
4. ユーザーAは 「101」 を取得してしまう。

ユーザーAは「自分のデータ(100)」に対して処理をしたいのに、誤って「他人のデータ(101)」を更新・削除してしまう。これがレースコンディション(競合状態)です。マルチユーザー環境のシステムでは、これは「バグ」ではなく「事故」です。

2. `@@IDENTITY` と `SCOPE_IDENTITY()` の決定的な違い

次に、SQL Serverのシステム関数を使った比較です。

  • `@@IDENTITY`: 「この接続セッション内で、最後に生成されたID」を返します。
  • `SCOPE_IDENTITY()`: 「現在のスコープ(実行中のプログラムの範囲)内で、最後に生成されたID」を返します。

「じゃあ `@@IDENTITY` でいいじゃないか」と思うかもしれません。しかし、ここに「トリガー」という伏兵が潜んでいます。

もし、`T_Orders` テーブルに「データが入ったら自動的にログテーブルにも記録する」というトリガーが設定されていたらどうなるでしょう?
`@@IDENTITY` は、トリガーによってログテーブルに書き込まれた「ログのID」を返してしまいます。

プロは必ず `SCOPE_IDENTITY()` を使います。 これにより、自分が投げたINSERT文によって生成されたIDだけを、ピンポイントで、安全に特定できるのです。

3. 実践:ADOのCommandオブジェクトを用いた黄金の実装

では、具体的にどう書くのが正解なのか。
最もエレガントで、最もパフォーマンスが高いのは、「INSERT文とSELECT文を一つのセットにして、ADOのCommandオブジェクトで実行する」方法です。

単に `Connection.Execute` を使うのではなく、`Command` オブジェクトを使って「結果セット(Recordset)」として受け取るのがポイントです。

【正解】CommandオブジェクトによるID取得パターン

''' <summary>
''' 新規オーダーを登録し、生成されたOrderIDを確実に取得する
''' </summary>
''' <param name="strCustomerName">顧客名</param>
''' <returns>生成されたOrderID</returns>
Public Function AddOrderAndGetID(ByVal strCustomerName As String) As Long
    Dim cn As ADODB.Connection
    Dim cmd As ADODB.Command
    Dim rs As ADODB.Recordset
    Dim strSQL As String
    Dim lngNewID As Long

    ' 接続文字列(環境に合わせて書き換えてください)
    Dim strConn As String
    strConn = "Provider=SQLNCLI11;Data Source=ServerName;Initial Catalog=DbName;Integrated Security=SSPI;"

    On Error GoTo ErrorHandler

    Set cn = New ADODB.Connection
    cn.Open strConn

    ' 【重要】INSERT文の直後に SCOPE_IDENTITY() を呼び出すSQLを構築
    ' セミコロンで繋ぐことで、一連の処理(アトミックな操作)として実行させる
    strSQL = "INSERT INTO T_Orders (CustomerName) VALUES ('" & strCustomerName & "'); " & _
             "SELECT SCOPE_IDENTITY() AS NewID;"

    ' Commandオブジェクトの生成
    Set cmd = New ADODB.Command
    With cmd
        .ActiveConnection = cn
        .CommandText = strSQL
        .CommandType = adCmdText
        
        ' 【重要】ExecuteメソッドでRecordsetとして結果を受け取る
        ' これにより、SELECT SCOPE_IDENTITY() の結果を直接読み取れる
        Set rs = .Execute
    End With

    ' 結果セットからIDを取り出す
    If Not rs.EOF Then
        lngNewID = rs.Fields("NewID").Value
    End If

    ' 後片付け
    rs.Close
    cn.Close

    AddOrderAndGetID = lngNewID
    Exit Function

ErrorHandler:
    MsgBox "エラーが発生しました: " & Err.Description, vbCritical
    If Not rs Is Nothing Then If rs.State = adStateOpen Then rs.Close
    If Not cn Is Nothing Then If cn.State = adStateOpen Then cn.Close
    AddOrderAndGetID = 0
End Function

このコードの「凄み」を解説

1. アトミックな実行: `INSERT` と `SELECT` を `;` で繋いで一つの文字列にしています。これにより、SQL Server側で「一つの命令」として処理され、他のユーザーの割り込みが入る余地を物理的に排除しています。
2. Commandオブジェクトの活用: `Connection.Execute` は「実行して終わり」ですが、`Command.Execute` は「実行した結果(Recordset)を返す」ことができます。これにより、サーバーから返ってきたIDを、あたかもSELECT文の結果のようにスマートに受け取れます。
3. スコープの限定: `SCOPE_IDENTITY()` を使っているため、もしテーブルにトリガーが仕掛けられていても、絶対に影響を受けません。

4. まとめ:アーキテクトからのメッセージ

Access VBAからSQL Serverへ。この移行は、単なる「データの置き場所の変更」ではありません。「同時実行性(Concurrency)」と「整合性(Integrity)」を意識した、プロフェッショナルな設計への入り口です。

  • `MAX(ID)` は、マルチユーザー環境では「凶器」になる。
  • `@@IDENTITY` は、トリガーによって「嘘」をつく。
  • `SCOPE_IDENTITY()` と `Command` オブジェクトの組み合わせこそが、真実を掴む唯一の手段。

「とりあえず動く」コードを書く段階は、もう卒業です。
次に目指すのは、「100人が同時に叩いても、1ミリの狂いもなく、正確に動作する」堅牢なシステムです。

この知識を武器に、ぜひ自信を持って開発を進めてください。応援していますよ。

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