マクロの記録から卒業する。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ミリの狂いもなく、正確に動作する」堅牢なシステムです。
この知識を武器に、ぜひ自信を持って開発を進めてください。応援していますよ。
