@@IDENTITY危機の管理

 

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

ID 値SQL Server管理する方法

ID 列を持つテーブルに行を追加する場合、ID 列の値は含まれません。これは、DBMS サーバーによって自動的にテーブル内の最も高い ID 値に増分 (通常は 1) が追加され、この値が新しい行の ID 値に使用されるためです。 SQL Serverを使用している場合、値は接続グローバル変数 (@@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 に 1 から 10 までの ID 値があり、 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 値が子外部キー値に反映されます。 リスト 1 は、 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 が新しい行の ID を取得するための追加のSQLを生成するのに役立ちますが、CommandBuilder はこの点でまったく役に立たないので、まったく役に立たなくなります。 私は通常、独自のアクションクエリSQLをロールするので、これらのアプローチのどちらもあまり役に立たない。

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

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

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

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

リスト 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 double-query メソッドを使用する場合、ADO.NET これらの追加の結果セットを検索し、変更を OUTPUT パラメーターから取得した場合でも DataRow に投稿します。 UpdatedRowSource プロパティは、この動作を制御します。 結果は? DataRow オブジェクトの ID 列は、自動的に新しいサーバー側の値に設定されます。 独自の InsertCommand をロールする場合は、ID 値を処理する ADO.NET が予想される場合は、このアプローチを模倣する必要があります。

コード例

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

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

いいえ、5行しかない場合でも、すべての親とすべての子行を返すとは思いません。 確かに、ADO.NET これを行うことができますが、10,000人の親と50,000人の子供がいる場合、このアプローチは特にうまく機能しません。 はい。親行をフェッチするときにも、パラメーター クエリを含めると良いでしょう。 このコードでは、親テーブルと子テーブルを維持するために使用されるアクション コマンドをハードコーディングします。 この 6 つのコマンド オブジェクトは、最初は 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

著者について

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