このトピックはまだ評価されていません - このトピックを評価する

@@IDENTITY クライシスを管理する

William Vaughn
Beta V Corporation

April 2003
日本語版最終更新日 2003 年 10 月 23 日

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

要約: INSERT を実行した後に新規作成 ID 値を返す方法を説明します。新規作成した ID 値の取得方法を知らなければ、SQL サーバーに ID 値を生成させると作業が困難になる可能性があります。

IdentityCrisis.msi サンプル ファイルのダウンロード

目次

ID 列の概要
Microsoft Access/JET の問題
サンプル コード

ご存知の方もおられますが、著者はニュースグループをサーフィンして興味深いトピックを見つけるのに 1 日のある程度の時間を費やします。最近では、JET データベース エンジンおよび Microsoft Access 関連の質問には近づかないようにしています。偏見を持ってしまって、それらの質問に答えると胃が痛むからです。「そんな質問をするということは、おもちゃのデータベース エンジンを使っているんだろう」と思ってしまうのです。しかし、まあこれは別の記事の素材としてとっておきましょう。毎週、新しい ID 値の処理方法に関する質問がとりあげられているようです。たとえば、データベース テーブルに ID 列を含む新しい行を追加した場合、サーバーによって ID 列に割り当てられた数値を知る方法などです。

この記事では、"INSERT 後フェッチ" という方法をご紹介します。これは、INSERT 実行後、新規作成された ID 値を返す追加の SELECT クエリを実行します。しかし、ID 値の処理は、新規生成値をフェッチするだけのことではありません。クライアント側で作成された値もあります。親子関係を作成し、テーブル間の関係を管理するために有効な ID 値が必要な場合、それらの値をどのように管理できるでしょうか。このことについても説明します。

ID 列の概要

ID に関する詳細を説明する前に、基本的な概念を少し説明します。ID 列は、「サーバーの範囲内で」一意である整数値をテーブルで提供するために使用します。それ以上のものではありません。ですから、世界中に散らばった複数のサーバーを使用している場合、ボストンの "CustomerOrders" テーブルで生成された ID 値が、クリーブランドの同名テーブルで生成された値と競合しないという保証はありません。数値が世界中で一意であることを確実にするには、別の修飾子 (システム ID など) を使用する必要があります。GUID UNIQUEIDENTIFIER の使用を検討してください (GUID 修飾子は、Microsoft SQL Server およびその他の多機能 DBMS システムでサポートされていますが、Microsoft Access/JET ではサポートされていません)。この記事内では、単一の DBMS サーバーを使用していると仮定し、別サーバーのデータベース複製については考えないことにします。

SQL Server で ID 値を管理する

ID 列を含む行をテーブルに追加したとき、ID 列には値を入力しません。そのテーブルの最大値に増分 (通常は 1) を加えた数値が、新しい行の ID 値として DBMS サーバーによって自動入力されるからです。SQL Server を使用している場合、この値は接続グローバル変数 @@IDENTITY にも保存されます (GUID 識別子は、SQL Server およびその他の多機能 DBMS システムでサポートされていますが、Access/JET ではサポートされていません)。それはいいのですが、行を削除した場合どうなるのでしょう。その行の値は永久に孤立してしまうのでしょうか。実はそうなります。ID をリシード (DBCC CHECKIDENT) しない限り、削除された ID は失われてしまいます。トランザクションがロールバックされた場合も、ID 値は孤立します。そのため、ID 列を使用する場合、連続した値のうち欠落部分が生じる可能性があります。また、使用している整数がいずれオーバーフローする可能性もあります。現在だけでなく将来のことも見こして、十分大きな整数を使用することが重要です。SQL Server の "integer" は、約 20 億の行を識別できます。また、"bigint" は、9,223,372,036,854,775,807 行 (かなり多くの行ですね) を識別できます。しかし、"smallint" では約 32000 行しか識別できません。ID 値が足りないという不平を聞いたことがあります。その人は 255 行しかない "tinyint" を使用していました。溜息がでます。孤立した ID 値の復元方法についてはここでは説明しません。説明が困難ですし、長年の経験から説明する価値もないとわかっていますので。次世紀または会社が合併吸収されるまで (そのときは全部書き直しになるでしょう) 足りる十分な桁数の整数を使用してください。

ADO.NET で ID 値を管理する

ADO.NET には、クライアント側 ID 値を処理する独自のメカニズムがあります。ADO.NET では、「分離された」 データを処理し、「実際の」 サーバー側データ テーブルとライブ アクセスすることは想定していないからです。このため、クライアント側で DataTable オブジェクトに行を追加しても、ADO.NET によって 「ローカルに」 生成された ID 値は、データベースの既存行やローカル DataTable の行の ID 値とは何の関係も持ちません。ちょっとおかしいと思われるかもしれません。どうやって機能するのでしょう。実は、ADO.NET では、生成した ID 値をクライアント側非接続テーブルの ID 値と照合することはしません。AutoIncrementSeed および AutoIncrementStep に基づいて新規 ID 値を設定します。既存 DataTable に 1 から 10 までの ID 値があるとします。AutoIncrementSeed の値を 10 に設定すると、ADO.NET は命令されたこと、つまり 10 から開始し、2 つの行で同じ ID 値が使用されることになり、好ましくありません。これらの "autoincrement" プロパティは、行を DataTable に追加する前に設定することができます。追加後は、関係ありません。この記事のデモ アプリケーションで、この動作を説明します。

ADO.NET での 「コツ」

クライアント側 ID 値を設定するとき、サーバー側データベース テーブルが使用していない、または他のクライアントによって追加される行が使用しないと推定できる値を指定することがコツです。この方法により、新規行が現在の DataTable の既存行と競合することを防ぐことができます。サーバー側 ID 値は通常、正の整数なので、クライアント側で負の整数を使用することができます。ADO.NET ではこの方法に対応できます。AutoIncrementSeed および AutoIncrementStep の両方を -1 に設定します。そうすると、新規生成行はクライアントで一意となります。またこれにより、INSERT クエリを実行するときにサーバーが 「実際の」 ID を割り当てる前に、親子関係の行の関係が識別しやすくなります。ADO.NET が UPDATE メソッドでサーバー側データベースに新規行を挿入するまで、親と子を関連付けるためにこれらの負の整数を使用することができます。ユーザーまたは ADO.NET が CommandBuilder を使用して生成する INSERT ステートメントには、クライアント側 ID 値は含まれません。クライアント側 ID 値は、クライアント側の行と関係を識別するためだけに必要です。

親子関係

新規親行 (「カスタマー」 テーブルの行など) を追加してその新規 ID 値を ADO.NET に生成させると、その子の行 (「オーダー」 など) を必要なだけ作成し、親の ADO.NET 生成 ID 値を外部キーとして安全に使用できます (子行は、正しい親と接続します)。ADO.NET では、これらの新規行をサーバーに転記するとき、この親子関係を正しく処理することができます。UPDATE メソッドを実行すると、ADO.NET は INSERT をまず親行に実行してから、関連付けられた子行すべてに実行します。InsertCommand を正しく設定してあれば、サーバー生成 ID 値は子の外部キー値に伝播されます。リスト 1 は、UPDATE メソッドを正しい順序で複数回実行する [更新] ボタン クリック イベント ハンドラの 「マジック」 部分を示すものです。最初に UPDATE メソッドが起動されたとき、新規親行を追加して既存の行を更新しますが、削除マークの付いたものは DataSet に残します。2 回目の UPDATE で、子テーブルの変更 (追加、変更、削除) をすべて行います。この方法により、子は親の後で追加されます。削除の場合は反対の順番となります。最後の UPDATE で、削除マークの付いた親行が削除されます。

リスト 1. UPDATE メソッドの正しいシーケンス

Try
    ' 最初に親、次に子を追加します。
    ' 最初に子、次に親を削除します。
    ' SELECT メソッドを使用して、更新または追加する
    ' 行の配列を返します。
  daParent.Update(ds.Tables(eTbl.Parent).Select("", "", _
    DataViewRowState.Added Or DataViewRowState.ModifiedCurrent))
   ' 子を追加、変更、または削除します。
  daChild.Update(ds.Tables(eTbl.Child))  
  ' 残っている親を削除します。
  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 の行セットの行すべてではありません。DataAdapter オブジェクトの InsertCommand で実行される INSERT SQL コマンドで、DACW で生成されたコードを利用するか、同等のコードを追加することを考えてみましょう。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, Stat FROM TestInsert WHERE (ID = @@IDENTITY)"
...'

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

@@IDENTITY に関する問題

ただ残念なことに、DACW 生成のコードでは、INSERT 実行時にデータベースはトリガ発生を予期していないと想定しています。トリガが発生し、新たな行がテーブルに追加された場合、@@IDENTITY グローバル変数は INSERT によって生成されたものではなく、新規 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, Stat FROM TestInsert WHERE (ID = SCOPE_IDENTITY())"
...'

残念なことに DACW にはこのオプションが用意されていません。

Microsoft Access/JET の問題

SQL Server や他の強力な DBMS システムとは異なり、JET データベース エンジン (Microsoft Access およびその他の小規模アプリケーションで使用) では、単一バッチで複数のステートメントを実行できません。このため、DataAdapter の RowUpdated イベントを使用して別のクエリを実行します。JET では @@IDENTITY (Access 2000 およびそれ以降のバージョン) をサポートしているので、イベント ハンドラで "SELECT @@IDENTITY" クエリを指定して、新規 ID 値を取得することができます。DataRow の ID 列に、値を自分でコピーする必要があります。ADO.NET では自動入力されません。ユーザーが加えた変更であると ADO.NET が混乱しないように、値設定後、DataAdapter の AcceptChanges メソッドを必ず呼び出します。リスト 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

' RowUpdated イベントをトラップします。
Private Sub RowUpdatedSetIdentity(ByVal sender As Object, _
ByVal e As OleDbRowUpdatedEventArgs)
If e.Status = UpdateStatus.Continue AndAlso _
   e.StatementType = StatementType.Insert Then  
' これが INSERT 操作の場合...
' Post-Update クエリを実行して新規 @@Identity をフェッチします。
   e.Row("ID") = CInt(cmdGetIdentity.ExecuteScalar)  
   e.Row.AcceptChanges()
End If

ここで重要な点は、JET では @@IDENTITY 値を接続状態で管理するということです。各接続で、新規 ID がそれぞれ管理されているので、ID の競合が予防されています。

ADO.NET で INSERT 結果セットを管理する

詳細な説明は省きますが、上記の SQL Server ダブルクエリ メソッドを使用すると、ADO.NET はそれらの追加結果セットを探し、変更を DataRow に転記します。これは、OUTPUT パラメータから来たものでも同様に行います。UpdatedRowSource プロパティによって、この動作が制御されます。その結果は、DataRow オブジェクトの ID 列が、自動的にサーバー側の新しい値に設定されます。独自の InsertCommand をロールする場合、ADO.NET に ID 値を処理させるには、このアプローチを真似てください。

サンプル コード

この記事のポイントを図示するため、サンプル アプリケーションを記述しました。ID 問題のアプローチ方法に影響する他の問題についても触れています。サンプルでは、行を TestInsertParent および TestInsertChild という 2 つのテーブルから取得します。これらのテーブルおよびその関係は非常にシンプルで、コード化は簡単です。

PID 列 (ParentID) は親テーブルの一意キー、子テーブルの外部キーです。この関係はハードコーディングしてあり、Constraint を設定して 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 列挙
  Parent
  Child
End Enum
Private Sub Form1_Load(ByVal sender As System.Object,  _
    ByVal e As System.EventArgs) Handles MyBase.Load
Try         ' トラップの問題
' Biblio DB をお持ちでない場合は、ご使用のテスト DB (Pubs など) に変更してください。
' アプリケーションの [ファイル] メニューには、テスト テーブル作成のためのエントリがあります。
  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)
' MissingSchemaAction を複数の Fill で DataTable にデータを 
' 追加せずに、更新するように設定します。
   daChild.MissingSchemaAction = MissingSchemaAction.AddWithKey
   GenerateCommands()         ' アクション コマンドをビルドします。
Catch ex As Exception
      MsgBox(ex.ToString)
End Try
End Sub
Private Sub CreateRelations()
    ' 親テーブルと子テーブルの間で、DataTable 間関係オブジェクトを作成します。
    Dim colParent As DataColumn =  _
    ds.Tables(eTbl.Parent).Columns("PID")
    Dim colChild As DataColumn = ds.Tables(eTbl.Child).Columns("PID")
    ' 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
     ' 最初に親、次に子を追加します。
     ' 最初に子、次に親を削除します。
     ' SELECT メソッドを使用して、更新または追加する
     ' 行の配列を返します。
     daParent.Update(ds.Tables(eTbl.Parent).Select("", "", _
        DataViewRowState.Added Or DataViewRowState.ModifiedCurrent))
     ' 子を追加、変更、または削除します。
     daChild.Update(ds.Tables(eTbl.Child))  
     ' 残っている親を削除します。
     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
   ' Autoincrement、シード、ステップを設定します。
   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()
   ' これらのコマンドは最初、DACW で生成してから、
to remove
   ' この記事で説明した "不具合" を修正してあります。
   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
 ' Name、データ型、サイズ、ソース列を設定します。
 .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
 ' 子 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
     ' Name、データ型、サイズ、ソース列を設定します。
     .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
 ' このルーチンは、テスト テーブルを作成するのに使用します。
 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  ")

  ' 親への PK/FK 関係で子テーブルを作成します。
  .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() ' DataSet の以前の内容をクリアします。 
                ' (親および子テーブルがクリアされました。)
     daParent.Fill(ds, "TestInsertParent")      ' 親 DataSet に入力します。
     If daParent.TableMappings.Count = 0 Then
  ' 論理を物理にマッピングします。
 daParent.TableMappings.Add("Table", "TestInsertParent")   
     End If
' 親 D を DataGrid にバインドします。
     dgDisplay.DataSource = ds.Tables(eTbl.Parent)  
     '   dgDisplay.DataMember = "TestInsertParent"
' AutoIncrement 値を設定します。    
     btnSetAutoIncrement.PerformClick()  
     ' 選択した親に基づき、子テーブル データをフェッチします。
     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
     ' DataGrid の現在の行から PID を抽出します。
     intPIDSelected = CInt(ds.Tables(eTbl.Parent).Rows(dgDisplay.CurrentRowIndex)("PID"))
' PID の子を検索します。    
daChild.SelectCommand.Parameters(0).Value = intPIDSelected    
' PID の子を検索します。    
' データベースから子 DataSet を入力します。
     daChild.Fill(ds, "TestInsertChild")           
'    dgDisplay.DataSource = ds.Tables(eTbl.Parent)      
' リバインド
     dgDisplay.Update()
     ' 子の論理テーブルから物理テーブルにマッピングします。
     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 e.KeyValue = 46 Then ' キーを削除します。
' 親を削除した場合、子行も必ず削除します。
  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 は、 Beta V Corporation  の社長です。Beta V Corporation は、ワシントン州レドモンドにあり、Microsoft キャンパスからそう離れていません。世界中のクライアントに、Visual Basic および SQL Server データ アクセス構造、ベスト プラクティスに特化したトレーニングやコンサルティング サービスを提供しています。近著に、『ADO.NET and ADO Examples and Best Practices for Visual Basic Programmers - 2nd Edition』、および C# バージョンの『ADO.NET Examples and Best Practices for C# Programmers』があります。どちらも、Apress でお求めになれます。他にも多くの記事やトレーニング コースを著しており、いくつかの国際的なコンピュータ関連コンファレンスで講演者としても高く評価されています。著者のメール アドレスは billva@nwlink.com です。

この情報は役に立ちましたか。
(残り 1500 文字)