@@IDENTITY危機の管理

 

William Vaughn
Beta V Corporation

2003 年 4 月

適用対象:
   Microsoft® ADO.NET
   Microsoft SQL Server™

概要: INSERT の実行後に新しく作成された ID 値を返す方法について説明します。 ID 値SQL Server生成すると、新しい ID 値を取得する方法がわからない場合、ジョブが困難になる可能性があります。 (17ページ印刷)

IdentityCrisis.msi サンプル ファイルをダウンロードします

内容

ID 列とは
Microsoft Access/JET の問題
コード例

ご存知のように、私は興味深いトピックのためにニュースグループ釣りを一日の一部を費やしています。 今日、私は JET データベース エンジンや Microsoft® Access の質問から離れています。私は、胃の中に結び目を入れることなく、これらの人々に答えるのに十分な公平性がなくなりました。 私は、「おもちゃのデータベース エンジンを使用していない場合は、この質問をすることはないでしょう」と考え続けます。しかし、それは別の記事の飼料です。 通常、毎週、新しい ID 値の処理方法について説明するいくつかの質問があります。 たとえば、ID 列を含むデータベース テーブルに新しい行を追加すると、ID 列に割り当てられたサーバーの番号をどのように確認できますか?

この記事では、INSERT の実行後に新しく作成された ID 値を返す追加の SELECT クエリを実行する際に、"POST-INSERT fetching" と呼ぶ戦略について説明します。 ただし、新しく生成された値を単にフェッチするよりも、ID 値を処理する方が多いです。 クライアントで作成された値はどうですか? 親子リレーションシップを作成し、テーブル間リレーションシップを管理するために有効な ID 値が必要な場合、これらの値を管理するにはどうすればよいですか? これについても説明します。

ID 列とは

ID の詳細の沼地に入る前に、基礎に関する最新情報のない概念をいくつか明らかにしましょう。 Identity 列は、 サーバーのスコープ内のテーブル内で一意であることが保証される一意の整数値を提供するために使用されます。それ以上は指定しません。 つまり、複数のサーバーが世界中に分散している場合、ボストンの "CustomerOrders" テーブルに対して生成された ID 値が、クリーブランドの別の同じデータベース内の同じテーブルに対して生成された値と競合しないという保証はありません。 つまり、世界的に一意であることが保証されている一意の数値が必要な場合は、さらに修飾子 (システム ID など) を指定しないと ID を使用できないため、代わりに GUID UNIQUEIDENTIFIER の使用を検討する必要があります。 (GUID 識別子は、Microsoft® SQL Server ™ およびその他のフル機能を備えた DBMS システムではサポートされていますが、Microsoft Access/JET ではサポートされていません)。この記事のコンテキストでは、単一の DBMS サーバーを操作していて、別のサーバーのデータベースとのレプリケートを気にしないことを前提にしましょう。

ID 値SQL Server管理する方法

ID 列を含むテーブルに行を追加する場合、DBMS サーバーは自動的に増分 (通常は 1) をテーブルの最も高い ID 値に追加し、この値を新しい行の ID 値に使用するため、Identity 列の値は含まれません。 SQL Serverを使用している場合、値は connection-global 変数 (@@IDENTITY) にも保存されます。 (GUID 識別子は、SQL Serverおよびその他のフル機能を備えた DBMS システムではサポートされていますが、Access/JET ではサポートされていません)。問題ありませんが、行が削除されるとどうなりますか? その行の ID 値は永遠に孤立していますか? うん、ID (DBCC CHECKIDENT) を再シードしない限り、削除された ID 値は失われます。 トランザクションがロールバックされると、ID 値も孤立します。 つまり、ID 列を使用する場合は、系列のギャップに備える必要があります。 また、最終的には、使用している整数がオーバーフローするため、ニーズに合わせて十分な大きさの整数を使用することが重要です。現在も将来も同様です。 SQL Serverの "整数" は約 20 億行を識別できますが、"bigint" は 9,223,372,036,854,775,807 行 (多数の行) を識別できます。 ただし、"smallint" は約 32,000 行しか識別できません。 私は実際に誰かが彼がID値を使い果たしたと不平を言いました。彼らは255行後に使い果たした「tinyint」を使用していました。 ため息。 孤立した ID 値を回復する手法を掘り下げるつもりはありません。 とにかくすべてを書き直すときに、次の世紀や企業の引き継ぎを得るのに十分な幅の整数を定義してください。

ID 値 ADO.NET 処理する方法

ADO.NET には、クライアント側の ID 値を処理する独自のメカニズムがあります。これは、ADO.NET が "切断された" データで動作し、"実際の" サーバー側データ テーブルへのライブ アクセスを想定していないためです。 つまり、クライアント上の DataTable オブジェクトに行を追加すると、ADO.NET によって ローカルに 生成された ID 値は、データベース内の既存の行の ID 値 ローカル DataTable の行に影響を及ぼしません。 どういうことでしょうか。 どのように動作しますか? ADO.NET は、クライアント側の切断されたテーブル内の既存の ID 値に対して生成される ID 値をテストする作業を行いません。 AutoIncrementSeed と AutoIncrementStep に基づいて新しい ID 値を設定します。 つまり、既存の DataTable の ID 値が 1 から 10 の場合、 AutoIncrementSeed 値を 10 に設定すると、ADO.NET が伝えた処理が行われます。これは 10 から始まり、最終的に同じ ID 値を持つ 2 行になります (適切ではありません)。 これらの "autoincrement" プロパティは、DataTable に行を追加する前に設定できます。その後は重要ではないようです。 この記事に含まれるデモ アプリケーションは、この動作を示しています。

"トリッキー" ADO.NET

クライアント側の ID 値を設定する場合のトリックは、サーバー側データベース テーブルで使用 されていない 値、または他のクライアントによって追加される行によって使用される可能性が高くない値を設定することです。 そうすることで、新しい行が現在の DataTable 内の既存の行と競合することはありません。 通常、サーバー側の ID 値は正の整数であるため、クライアントで使用する数値のセットは他に 1 つだけです。負の数です。 ADO.NET は、このコンティンジェンシーを処理する準備が整いました。 AutoIncrementSeedAutoIncrementStep の 両方を -1 に設定します。 このようにして、各新しい行は、クライアントに固有の ID で生成されます。 これは、INSERT クエリを実行するときに、サーバーが "実際の" ID を割り当てる前に、親子リレーションシップの行がそのリレーションシップを簡単に識別できることを意味します。 これらの負の数は、 Update メソッドを使用して新しい行 ADO.NET サーバー側データベースに挿入されるまで、親を子と相互に変換するために使用されます。 ( CommandBuilder を介して) 生成 ADO.NET INSERT ステートメントには、クライアント側の ID 値は含 まれていない ため、クライアント側の行とリレーションシップを識別するためにのみ必要です。

Parent-Childリレーションシップ

新しい親行 (たとえば、"Customer" テーブルの行) を作成し、新しい ID 値を生成 ADO.NET したら、必要な数の子行 ("Orders" など) を作成し、親の ADO.NET 生成された ID 値を外部キーとして安全に使用できます (そのため、子行は正しい親に関連付けられます)。 はい。ADO.NET は、これらの新しい行をサーバーに投稿するときに、これらのリレーションシップを正しく処理する方法を認識しています。 Update メソッドを実行すると、ADO.NET 最初に親行に対して INSERT が実行され、次に関連付けられているすべての子行が実行されます。 InsertCommand を正しく設定すると、サーバーによって生成された ID 値が子外部キー値に反映されます。 Listing1 は、 Update メソッドを右のシーケンスで複数回実行する Update ボタン クリック イベント ハンドラーの "マジック" 部分を示しています。 Update が初めて呼び出されたときに、新しい行を追加し、既存の親行を更新しますが、削除対象としてマークされた行は DataSet に残ります。 Update I への 2 回目の呼び出しでは、子テーブルに対するすべての変更 (追加、変更、または削除) を行います。 これにより、子は親の後に追加されますが、親が削除された後にのみ削除されます。 最後の更新では、削除対象としてマークされた親行が削除され、子が最初 (親の前) に削除されます。

リスト 1。 正しい Update メソッドのシーケンス処理

Try
    ' Add parents first, then children
    ' Delete children first, then parents
    ' Use the Select method to return an array of rows
    '  to be updated or added
  daParent.Update(ds.Tables(eTbl.Parent).Select("", "", _
    DataViewRowState.Added Or DataViewRowState.ModifiedCurrent))
   ' Add, change or delete children
  daChild.Update(ds.Tables(eTbl.Child))  
  ' Delete any remaining parents
  daParent.Update(ds.Tables(eTbl.Parent)) 

ヒント いいえ、このコードでは AcceptChanges メソッドの呼び出しは表示されません。データベースへの変更をポストした後に Update メソッドによって自動的に呼び出されるため、必要ありません。

新しい ID 値の取得

実際の問題は、サーバー側の DBMS エンジンによって生成された ID 値を調べる必要がある場合に発生します。 残念ながら、ADO.NET はそれ自体を助けるために何もしません。

Visual Studio® DataAdapter 構成ウィザード (DACW) は 、InsertCommand の追加 SQL を生成して新しい行の ID を取得するのに役立ちますが、 CommandBuilder はこの点で手がかりがないため、まったく役に立ちません。 私は通常、自分のアクションクエリSQLをロールするので、これらのアプローチはどちらも役に立たない。少なくともそれほど役に立たない。

1 つの方法は、単にクエリを再実行することです。 つまり、 SelectCommand 全体を再実行して DataSet を再構築または更新しますが、これはやり過ぎです。 必要なのは、DataTable の行セット内のすべての行ではなく、新しい行です。 DACW のリードを取得すると、生成されたコードを活用するか、独自の INSERT SQL コマンドに同等のコードを追加して、DataAdapter オブジェクトの InsertCommand によって実行できます。 新しい ID を返すために追加の SQL を生成するために DACW を取得するには、DACW ウィザードを実行する以外に何も行う必要はありません。既定の "詳細設定" 設定で行います。

図 1. DataAdapter 構成ウィザードの [詳細設定] オプション

INSERT を実行し、新しい ID 値を取得するために DACW によって生成されるコードの例をリスト 2 に示します。 DACW を実行した後、Windows フォーム Designerによって生成されたコード領域にドリルダウンして見つけます。

一覧 2. DACW によって生成される InsertCommand

#Region " Windows Form Designer generated code "
…
'SqlInsertCommand1
Me.SqlInsertCommand1.CommandText = "INSERT INTO TestInsert(Name, State)  _"
& "VALUES (@Name, @State);" _
& " SELECT ID, Name, State FROM TestInsert WHERE (ID = @@IDENTITY)"
…'

DACW では、INSERT の後に別の SQL SELECT ステートメントが追加され、新しい行からすべての値 (新しい ID 値を含む) が返されることに注意してください。

@@IDENTITYに関する問題

残念ながら、DACW で生成されたコードは、INSERT の実行時にトリガーが発生することをデータベースが想定していないことを前提としています。 トリガーが起動し、そのトリガーによってテーブルに別の行が追加された場合、@@IDENTITYグローバル変数は、INSERT によって生成された ID 値ではなく、その新しい ID 値を指す値に設定されます。 これにより、DACW で生成されたコードは単純な状況では機能しますが、データベースがより高度になるときには機能しません。 うまくいけば、それはあなたが退職するまで起こりません。 解決策は? エラーが発生しやすい@@IDENTITYグローバル変数を使用する代わりに、コードで新しい SCOPE_IDENTITY() 関数を使用する必要があります。 最も内側の ID 値が返され、トリガーなどの他のコード スコープで実行される他の INSERT 操作の影響を受けません。 このコードは、少なくとも SQL Server ™ 2000 以降のリスト 3 のようになります。

一覧 3. InsertCommand を修正しました

'SqlInsertCommand1
Me.SqlInsertCommand1.CommandText = "INSERT INTO TestInsert(Name, State)  _"
& "VALUES (@Name, @State);" _
& " SELECT ID, Name, State FROM TestInsert WHERE (ID = SCOPE_IDENTITY())"
…'

あまりにも悪い DACW は、このオプションを提供しません。

Microsoft Access/JET の問題

SQL Serverやその他の強力な DBMS システムとは異なり、JET データベース エンジン (Microsoft® Access やその他の小規模なアプリケーションで使用される) では、1 つのバッチで複数のステートメントを実行することはできません。 つまり、DataAdapter RowUpdated イベントを使用して別のクエリを実行する別のコースを受講する必要があります。 JET では(Access 2000 以降で) @@IDENTITYがサポートされているため、イベント ハンドラーで "SELECT @@IDENTITY" クエリを送信して、新しい ID 値をキャプチャできます。 値は自分で DataRow の [ID] 列にコピーする必要があります。ADO.NET は自動的には行われません。 値を設定した後で DataAdapter AcceptChanges メソッドを呼び出して、ADO.NET がユーザーが生成した変更であると思い込まないようにしてください。 リスト 4 は、イベント ハンドラーを作成し、新しい行の ID 値をキャプチャする方法を示す擬似コードを示しています。

Dim da As OleDbDataAdapter
Dim cn As OleDbConnection
Dim cmdGetIdentity As New OleDbCommand("SELECT @@IDENTITY", cn)
    

DataAdapter を宣言し、RowUpdated イベントのイベント ハンドラーを追加します。

一覧 4. RowUpdated イベントでの ID 値のキャプチャ

AddHandler da.RowUpdated, AddressOf RowUpdatedSetIdentity

' Trap RowUpdated event
Private Sub RowUpdatedSetIdentity(ByVal sender As Object, _
ByVal e As OleDbRowUpdatedEventArgs)
If e.Status = UpdateStatus.Continue AndAlso _
   e.StatementType = StatementType.Insert Then  
' If this is an INSERT operation...
' Execute the post-update query to fetch new @@Identity
   e.Row("ID") = CInt(cmdGetIdentity.ExecuteScalar)  
   e.Row.AcceptChanges()
End If

ここで重要な点は、JET が接続状態の@@IDENTITY値を管理することです。 つまり、各接続で新しい ID 値が独自に管理されるため、ID の競合を防ぐ必要があります。

INSERT 結果セット ADO.NET 管理する方法

詳細を説明せずに、前述のSQL Serverのダブルクエリ メソッドを使用する場合、ADO.NET はこれらの追加の結果セットを検索し、OUTPUT パラメーターから取得した場合でも、DataRow への変更を投稿します。 UpdatedRowSource プロパティは、この動作を制御します。 結果は? DataRow オブジェクトの Identity 列は、新しいサーバー側の値に自動的に設定されます。 独自の InsertCommand をロールする場合は、ID 値を処理する ADO.NET が予想される場合は、このアプローチを模倣します。

コード例

この記事のポイントを示すサンプル アプリケーションを作成しました。 また、ID の問題に対するアプローチ方法にも影響を与える可能性のある他のいくつかの問題にも対処します。 このサンプルでは、TestInsertParent と TestInsertChild の 2 つのテーブルから行を取得します。 テーブルとそのリレーションシップは、コーディングを簡単にするために非常に簡単です。

PID 列 (ParentID) は、親テーブルの一意のキーであり、子の外部キーです。 このリレーションシップをハードコーディングし、連鎖削除の問題を処理するための ADO.NET を取得するように制約を設定します。 つまり、親行を削除すると、子行も削除 ADO.NET 必要があります。 これを強制するために、テーブルを作成するときに(そうするコードはプログラムの一部です)、参照整合性の問題を防ぐためにサーバー側の制約も設定されていることを確認します。 DataGrid は、親行の初期行セットを表示するために使用されます。 ユーザーが DataGrid の "+" 記号をクリックして子行にドリルダウンすると、サンプルはクイック パラメーター クエリを実行して、関連する子を返します。

いいえ、5行しかない場合でも、すべての親とすべての子行を返すとは思いません。 確かに、ADO.NET これを行うことができますが、10,000 人の親と 50,000 人の子供がいる場合、このアプローチは特にうまく機能しません。 はい。親行をフェッチする場合は、パラメーター クエリも含めると便利です。 このコードでは、親テーブルと子テーブルを維持するために使用されるアクション コマンドをハードコーディングします。 これらの 6 つの Command オブジェクトは、DataAdapter 構成ウィザードを使用して最初に作成されましたが、より効率的で保守が容易になるように手動で調整されました。

一覧 5. 新しく追加された行に対する AutoIncrement 効果を示す

Dim cn As SqlConnection
Dim daParent, daChild As SqlDataAdapter
Dim ds As New DataSet()
Dim intPIDSelected As Integer = 0
Enum eTbl              ' DataSet Tables enumeration
  Parent
  Child
End Enum
Private Sub Form1_Load(ByVal sender As System.Object,  _
    ByVal e As System.EventArgs) Handles MyBase.Load
Try         ' Trap problems 
' If you don't have the Biblio DB, change this to your own test DB (like Pubs)
' The application File menu has an entry that creates the test tables.
  cn = New SqlConnection("server=demoserver;database=biblio;integrated security=sspi")
  daParent = New SqlDataAdapter("SELECT PID, Name, State" _
 & "FROM TestInsertParent", cn)
  daChild = New SqlDataAdapter("SELECT CID, PID, ChildName, ChildAge" _
   & " FROM TestInsertChild WHERE PID = @PIDWanted", cn)
   daChild.SelectCommand.Parameters.Add("@PIDWanted", SqlDbType.Int)
' Set MissingSchemaAction to make sure multiple Fills don't add, 
' but update data in DataTable
   daChild.MissingSchemaAction = MissingSchemaAction.AddWithKey
   GenerateCommands()         ' Build the Action Commands
Catch ex As Exception
      MsgBox(ex.ToString)
End Try
End Sub
Private Sub CreateRelations()
    ' Create inter-DataTable Relation objects between the Parent and Child tables
    Dim colParent As DataColumn =  _
    ds.Tables(eTbl.Parent).Columns("PID")
    Dim colChild As DataColumn = ds.Tables(eTbl.Child).Columns("PID")
    ' Create Relation along with Constraint
    Dim daRel As New DataRelation("ParentToChildRelation",  _
colParent, colChild, True)
    ds.Relations.Add(daRel)
End Sub
Private Sub btnUpdate_Click(ByVal sender As System.Object,  _
      ByVal e As System.EventArgs) Handles btnUpdate.Click
   Try
     ' Add parents first, then children
     ' Delete children first, then parents
     ' Use the Select method to return 
     ' an array of rows to be updated or added
     daParent.Update(ds.Tables(eTbl.Parent).Select("", "", _
        DataViewRowState.Added Or DataViewRowState.ModifiedCurrent))
     ' Add, change or delete children
     daChild.Update(ds.Tables(eTbl.Child))  
     ' Delete any remaining parents
     daParent.Update(ds.Tables(eTbl.Parent))   
   Catch exsql As SqlException
       If exsql.Number = 547 Then
       MsgBox("You must first delete child rows …. ")
       Else
         MsgBox(exsql.ToString)
       End If
   Catch ex As Exception
         MsgBox(ex.ToString)
   End Try
End Sub
Private Sub btnSetAutoIncrement_Click(ByVal sender  _
As System.Object, ByVal e As System.EventArgs) _
Handles btnSetAutoIncrement.Click
Try
   ' Set Autoincrement, seed, and step
   With ds.Tables(eTbl.Parent).Columns("PID")
     .AutoIncrement = True
     .AutoIncrementSeed = CInt(txtAutoIncrementSeed.Text)
     .AutoIncrementStep = CInt(txtAutoIncrementStep.Text)
   End With
Catch ex As Exception
End Try
End Sub
Private Sub GenerateCommands()
   ' These commands were originally generated by the DACW and tuned 
to remove
   ' some "imperfections" as discussed in the text of the article.
   daParent.InsertCommand = New SqlCommand()
With daParent.InsertCommand
 .CommandText = "INSERT INTO TestInsertParent(Name, State) " _ 
 & " VALUES (@Name, @State); " _
 & " SELECT PID, Name, State " _
 & " FROM TestInsertParent WHERE (PID = SCOPE_IDENTITY())"
 .Connection = cn
 ' Set Name, datatype, size and source column.
 .Parameters.Add("@Name", System.Data.SqlDbType.VarChar, 50, "Name")
 .Parameters.Add("@State", System.Data.SqlDbType.VarChar, 50, _ "State")
End With
 daParent.UpdateCommand = New SqlCommand()
 With daParent.UpdateCommand
   .CommandText = "UPDATE TestInsert SET Name = @Name, "  _
   & " State = @State " _
       & "WHERE (PID = @Original_PID) AND (Name = @Original_Name) " _
       & " AND       (State = @Original_State); " _
       & "SELECT PID, Name, State FROM TestInsertParent WHERE (PID = @PID)"
   .Connection = cn
   .Parameters.Add("@Name", System.Data.SqlDbType.VarChar, " _
       50, "Name")
   .Parameters.Add("@State", System.Data.SqlDbType.VarChar, " _
       50, "State")
   .Parameters.Add(New System.Data.SqlClient.SqlParameter( _
       "@Original_PID", System.Data.SqlDbType.Int, 4, System.Data.ParameterDirection.Input, False, _
   CType(0, Byte), CType(0, Byte),"PID", _
       System.Data.DataRowVersion.Original, Nothing))
   .Parameters.Add(New System.Data.SqlClient.SqlParameter(_
        "@Original_Name", System.Data.SqlDbType.VarChar, _
        50, System.Data.ParameterDirection.Input, _
        False, CType(0,Byte),CType(0,Byte),"Name", _
      System.Data.DataRowVersion.Original, Nothing))
   .Parameters.Add(New _ 
    System.Data.SqlClient.SqlParameter("@Original_State", _
    System.Data.SqlDbType.VarChar, 50, _
    System.Data.ParameterDirection.Input, _
    False, CType(0, Byte), CType(0, Byte), "State", _
    System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add("@PID", System.Data.SqlDbType.Int, 4, "PID")
 End With
 daParent.DeleteCommand = New SqlCommand()
 With daParent.DeleteCommand
     .CommandText = "DELETE FROM TestInsertParent " _
     & " WHERE (PID = @Original_PID) " _
     & "AND (Name = @Original_Name) AND (State = @Original_State)"
     .Connection = cn
     .Parameters.Add(New _ 
      System.Data.SqlClient.SqlParameter("@Original_PID", _
      System.Data.SqlDbType.Int, 4, _
      System.Data.ParameterDirection.Input, _
      False, CType(0, Byte), CType(0, Byte), "PID", _
      System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New _ 
       System.Data.SqlClient.SqlParameter("@Original_Name", _
       System.Data.SqlDbType.VarChar, 50, _
       System.Data.ParameterDirection.Input, _
       False, CType(0, Byte), CType(0, Byte), "Name", _
       System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New _
       System.Data.SqlClient.SqlParameter("@Original_State", _
       System.Data.SqlDbType.VarChar, 50, _
       System.Data.ParameterDirection.Input, _
       False, CType(0, Byte), CType(0, Byte), "State", _
       System.Data.DataRowVersion.Original, Nothing))
 End With
 ' Generate action Commands for the Child DataAdapter
 daChild.InsertCommand = New SqlCommand()
 With daChild.InsertCommand
     .CommandText = "INSERT INTO TestInsertChild(PID, " _
& " ChildName, ChildAge) " _
      & " VALUES (@PID, @ChildName, @ChildAge); " _
      & " SELECT CID, PID, ChildName, ChildAge " _
      & " FROM TestInsertChild WHERE (CID = SCOPE_IDENTITY())"
     .Connection = cn
     ' Set Name, datatype, size and source column.
     .Parameters.Add("@PID", System.Data.SqlDbType.Int, 2, "PID")
     .Parameters.Add("@ChildName", System.Data.SqlDbType.VarChar, _
 50, "ChildName")
     .Parameters.Add("@ChildAge", System.Data.SqlDbType.TinyInt, _
 1, "ChildAge")
 End With
 daChild.UpdateCommand = New SqlCommand()
 With daChild.UpdateCommand
     .CommandText = "UPDATE TestInsert SET PID = @PID, " _
      & " Name = @ChildName, ChildAge = @ChildAge " _
      & "WHERE (CID = @Original_CID) AND PID = @Original_PID) " _
      & " AND (ChildName = @Original_Name) AND " _
      & "(ChildAge = @ChildAge); " _
      & "SELECT CID, PID, ChildName, ChildAge " _
      & " FROM TestInsertChild WHERE (CID = @CID)"
     .Connection = cn
     .Parameters.Add("@PID", System.Data.SqlDbType.Int, 2, "PID")
     .Parameters.Add("@ChildName",  _
 System.Data.SqlDbType.VarChar, 50, "ChildName")
     .Parameters.Add("@ChildAge",  _
 System.Data.SqlDbType.TinyInt, 1, "ChildAge")
     .Parameters.Add(New _
       System.Data.SqlClient.SqlParameter("@Original_CID", _
       System.Data.SqlDbType.Int, 4,  _
       System.Data.ParameterDirection.Input, False, _
       CType(0, Byte), CType(0, Byte), "CID", _
       System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New _
       System.Data.SqlClient.SqlParameter("@Original_PID", _
       System.Data.SqlDbType.Int, 4,  _
       System.Data.ParameterDirection.Input, False, _
       CType(0, Byte), CType(0, Byte), "PID", _
       System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New _
      System.Data.SqlClient.SqlParameter("@Original_ChildName", _
      System.Data.SqlDbType.VarChar, 50, _ 
      System.Data.ParameterDirection.Input, _
      False, CType(0, Byte), CType(0, Byte), "ChildName", _
      System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New System.Data.SqlClient.SqlParameter("@Original_ChildAge", _
  System.Data.SqlDbType.TinyInt, 1, _
  System.Data.ParameterDirection.Input, _
  False, CType(0, Byte), CType(0, Byte), "ChildAge", _
  System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add("@CID", System.Data.SqlDbType.Int, 4, "CID")
 End With
 daChild.DeleteCommand = New SqlCommand()
 With daChild.DeleteCommand
     .CommandText = "DELETE FROM TestInsertChild " _
     & " WHERE (CID = @Original_CID) " _
     & " AND (PID = @Original_PID) " _
     & " AND (ChildName = @Original_ChildName) " _
     & " AND (ChildAge = @Original_ChildAge)"
     .Connection = cn
     .Parameters.Add(New _
       System.Data.SqlClient.SqlParameter("@Original_CID", _
       System.Data.SqlDbType.Int, 4, _
       System.Data.ParameterDirection.Input, _
       False, CType(0, Byte), CType(0, Byte), "CID", _
       System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New _ 
       System.Data.SqlClient.SqlParameter("@Original_PID", _
       System.Data.SqlDbType.Int, 4, _
       System.Data.ParameterDirection.Input, False, _
       CType(0, Byte), CType(0, Byte), "PID", _
       System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New _
       System.Data.SqlClient.SqlParameter("@Original_ChildName", _
       System.Data.SqlDbType.VarChar, 50, _
      System.Data.ParameterDirection.Input, _
      False, CType(0, Byte), CType(0, Byte), "ChildName", _
      System.Data.DataRowVersion.Original, Nothing))
     .Parameters.Add(New _
      System.Data.SqlClient.SqlParameter("@Original_ChildAge", _
      System.Data.SqlDbType.TinyInt, 1, _
      System.Data.ParameterDirection.Input, _
      False, CType(0, Byte), CType(0, Byte), "ChildAge",  _
      System.Data.DataRowVersion.Original, Nothing))
 End With
End Sub
Private Sub mnuCreateTables_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles mnuCreateTables.Click
 ' This routine is used to create the test tables. 
 Dim strMakeTable As New StringBuilder()
 Try
     With strMakeTable
  .Append("IF NOT EXISTS(SELECT * FROM Sysobjects " _
& " WHERE Name = 'TestInsertParent')")
  .Append("BEGIN CREATE TABLE [TestInsertParent] (")
  .Append("  [PID] [int] IDENTITY (1, 1) NOT NULL ,  ")
  .Append("  [Name] [varchar] (50)  NOT NULL ,   ")
  .Append("  [State] [varchar] (50)  NOT NULL ,   ")
  .Append("  CONSTRAINT [PK_TestInsertParent] "_ 
          & "  PRIMARY KEY  CLUSTERED   ")
  .Append(" ( [PID] ) ON [PRIMARY] ) ON [PRIMARY]    END  ")

  ' Create Child table with PK/FK relationship to Parent
  .Append("IF NOT EXISTS(SELECT * FROM Sysobjects " _
& " WHERE Name = 'TestInsertChild')")
  .Append("BEGIN CREATE TABLE [TestInsertChild] (")
  .Append("  [CID] [int] IDENTITY (1, 1) NOT NULL ,  ")
  .Append("  [PID] [int] NOT NULL ,  ")
  .Append("  [ChildName] [varchar] (50)  NOT NULL ,   ")
  .Append("  [ChildAge] [tinyint]  NOT NULL ,   ")
  .Append("  CONSTRAINT [PK_TestInsertChild] "_ 
        & "  PRIMARY KEY  CLUSTERED   ")
  .Append(" ( [CID] ) ON [PRIMARY], ")
  .Append(" CONSTRAINT [FK_TestInsertChild_TestInsertParent] ")
    .Append(" FOREIGN KEY([PID]) REFERENCES [TestInsertParent] "_ 
        & " ([PID])) ")
  .Append(" ON [PRIMARY] ")
  .Append(" END  ")
     End With
     cn.Open()
     Dim cmdMakeTable As New SqlCommand(strMakeTable.ToString, cn)
     cmdMakeTable.ExecuteNonQuery()
     MsgBox("TestInsert Table created...")
 Catch ex As Exception
     MsgBox(ex.ToString)
 Finally
     cn.Close()
 End Try
    End Sub
    Private Sub btnQuery_Click(ByVal sender As System.Object, _
     ByVal e As System.EventArgs) Handles btnQuery.Click
 Try
     ds.Clear() ' Clear out any previous contents of the DataSet  
                ' (Parent and Child tables cleared)
     daParent.Fill(ds, "TestInsertParent")      ' Fill the Parent DataSet
     If daParent.TableMappings.Count = 0 Then
  ' Map logical to physical
 daParent.TableMappings.Add("Table", "TestInsertParent")   
     End If
' Bind Parent Ds to the DataGrid
     dgDisplay.DataSource = ds.Tables(eTbl.Parent)  
     '   dgDisplay.DataMember = "TestInsertParent"
' Set the AutoIncrement values     
     btnSetAutoIncrement.PerformClick()  
     ' Fetch the Child table data based on the selected Parent
     If ds.Tables(eTbl.Parent).Rows.Count = 0 Then
        MsgBox("Query did not return any rows. " _
  & " You can add new rows to the Parent grid.")
     End If
     FetchChildRows()
     If ds.Relations.Count = 0 Then
  CreateRelations()
     End If
 Catch exSQL As SqlException
     If exSQL.Number = 208 Then
  MsgBox("Did you create the TestInsert table(s)? See the File menu.")
     Else
  MsgBox("Unexpected SqlExecption" & exSQL.ToString)
     End If
 Catch ex As Exception
     MsgBox(ex.ToString)
 End Try
    End Sub
Private Sub dgDisplay_Navigate(ByVal sender As Object, _
ByVal ne As System.Windows.Forms.NavigateEventArgs) _
 Handles dgDisplay.Navigate
 btnUpdate.Enabled = ds.HasChanges
 If ne.Forward Then
     FetchChildRows()
 End If
    End Sub
    Private Sub FetchChildRows()
 Try
     ' Extract PID from current row on DataGrid
     intPIDSelected = CInt(ds.Tables(eTbl.Parent).Rows(dgDisplay.CurrentRowIndex)("PID"))
' Search for PID's children     
daChild.SelectCommand.Parameters(0).Value = intPIDSelected    
' Search for PID's children
' Fill the Child DataSet from the database
     daChild.Fill(ds, "TestInsertChild")           
'    dgDisplay.DataSource = ds.Tables(eTbl.Parent)      
' Rebind
     dgDisplay.Update()
     ' Map logical Child table to physical table
     If daChild.TableMappings.Count = 1 Then 
      daChild.TableMappings.Add("Table1", "TestInsertChild")
     End If   
 Catch ex As Exception
     MsgBox(ex.ToString)
 End Try
    End Sub

Private Sub dgDisplay_KeyDown(ByVal sender As Object,  _
ByVal e As System.Windows.Forms.KeyEventArgs)  _
Handles dgDisplay.KeyDown
 ' If the user clicks Delete in the grid—
 ' be sure to fetch any child rows associated with this Parent
 ' to ensure that they get deleted before the parent.
 If e.KeyValue = 46 Then ' Delete key
' Make sure that the child rows are included when Parent is deleted
  FetchChildRows()
End If
    End Sub
    Private Sub mnyFileExit_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles mnuFileExit.Click
 If ds.HasChanges Then
     Dim resp As MsgBoxResult = MsgBox("Do you want to save " _
& "the changes made to your data?", MsgBoxStyle.YesNo  _
Or MsgBoxStyle.Question, "Uncommited data")
     If resp = MsgBoxResult.Yes Then
  btnUpdate.PerformClick()
     End If
 End If
 End
    End Sub
End Class

著者について

William (Bill) Vaughn は、Microsoft キャンパスから遠くないワシントン州レドモンドに拠点を置く ベータ V Corporation の社長です。 Visual Basic とSQL Serverデータ アクセス アーキテクチャとベスト プラクティスに特化したメンタリング、トレーニング、コンサルティング サービスを世界中のクライアントに提供しています。 William の最新の書籍は 、visual Basic Programmers の ADO.NET と ADO の例とベスト プラクティスです。第 2 版 と C# バージョン ADO.NET 例と C# プログラマのベスト プラクティスです。 どちらも Apress から入手できます。 ウィリアムはまた、多くの記事やトレーニングコースの著者であり、いくつかの国際コンピュータ会議で最高評価の講演者です。 あなたはで彼に billva@nwlink.com到達することができます.