データの関連付けとその関連機能
Dino Esposito
Wintellect
July 12, 2001
日本語版最終更新日 2001 年 9 月 25 日
MSDN Code Center からサンプル (ViewManager.exe) をダウンロードする
多くのアプリケーションでは、ほかのデータと何らかの関連があるデータを表示する必要があります。
これを説明するよく知られている例は、
有名な Orders (受注) テーブルへのリンクを必要とする同じく有名な Customers (得意先) テーブルによって提供されます。
この 2 つが共通に保持しているものは何でしょうか ?
そうです、これも有名な CustID (得意先コード) フィールドです。
指定した得意先に対するすべての受注を表示するという問題を通常どのように解決するのでしょうか ?
アプリケーションの制約と要件に応じて、多くの実現可能なソリューションを適用していることでしょう。
.NET では、新しい価値あるツールをプログラマ ツールキットに追加できます。
このツールは DataRelation オブジェクトです。
これは基本的に 2 つのテーブル間に設定される親子のリレーションシップを表します。
DataRelation それ自体はあまり大したものではありません。
しかし、DataSet やほかの ADO.NET のオブジェクトやコントロールがリレーションシップに対して持っているサポートの観点から見ると、
その重要性、有益性は増大します。
一般的なアプローチ
関連するテーブルからデータを取得する必要がある場合の一般的なソリューションは、
平凡な旧式の INNER JOIN SQL コマンドを使用することです。
このコマンドは、作業に使う必要のある 2 つの入力テーブルの列を 1 つの結果セットにマージします。
以下のコードは、
Customers の 2 列と Orders
の 3 列を見つける最終的な結果セットを作成します。
SELECT c.Name, c.City, o.Date, o.TotalPrice, oShipAddress
FROM Customers AS c
INNER JOIN Orders AS o
ON c.CustID = o.CustID
INNER JOIN ステートメントは、データベース サーバーを必要とし、
最終的に一定量の重複したデータを含む行を返します。
上記のクエリを実行するときは、ある得意先の受注すべてを取得および処理することを目的にしています。
そのため、得意先に関する情報、たとえば、番地、都市名などを繰り返し取得する必要はありません。
それにもかかわらず、こういった情報がまさに結果セットから表構造の形式で返されるものです。
ADO では、階層的で、それゆえに不規則な形式のレコードセットをデータ シェイプ サービスで作成できます。
データ シェイプを使うことにより、
Customer と Orders のリレーションシップはすべての得意先情報を持つ 1 行に、
子レコードセットを指すフィールドを加えた 1 行で表現されます。
その子レコードセットは、指定した得意先コードに関連付けられる受注ごとに 1 行保持します。
受注行の構造は、Orders テーブルからクエリしたフィールドによって決まります。
ADO データ シェイプは、SHAPE 言語と呼ばれる特別な言語を使ってクエリを記述します。
SHAPE
{SELECT Name, City, Custid FROM Employees}
APPEND (
{SELECT CustId, Date, TotalPrice, ShipAddress FROM Orders} AS oOrders
RELATE CustId TO CustId)
次に、ADO データシェイプは、1 つの接続内でデータベースで必要なクエリをすべて実行します。
結果は、クライアントに返す途中に特殊な OLE DB サービスによって階層化されたレコードセットに形式化され、
クライアント側に返されます。
INNER JOIN とデータ シェイプは、
関連するデータをフェッチおよび格納する方法で多くの作業を行う必要があります。
このデータをクライアント アプリケーションで取得し、表示したらどうでしょう ?
INNER JOIN ステートメントが返す結果は表構造になっていて、
必要な情報の抽出はすべてクライアントが行います。
データ シェイプを使うと、情報自体は適切なレイアウトになり、
本来の形式で表示されることになります。
得意先情報は受注リストとは独立しています。
特定の名前で普通のレコードセット フィールドとしてアクセスできます。
Set rsCustomerOrders = oRS.Fields("oOrders").Value
指定した得意先の受注データにアクセスするには、
Customers テーブルの対応する行を選択し、
あらかじめ設定されたリレーションに一致する名前のフィールドにアクセスします。
ADO.NET 型のデータ シェイプ
ADO.NET で親子データのリレーションシップを表すために、
DataRelation オブジェクトを使用できることを期待します。
ADO データ シェイプに精通しているの場合は、
DataRelation オブジェクトに隠ぺいされている、
上記で示した SHAPE 言語のコードをすぐに理解できるでしょう。
ADO.NET では、
DataRelation オブジェクトは 2 つの DataTable オブジェクト間のインメモリ
リレーションシップを確立するために使用されます。
リレーションシップは 2 つのテーブルが共通に持っている 1 列で見つかる一致する値に設定されます。
ADO.NET の列は DataColumn オブジェクトで表されます。
前半で示した Customer と Orders のリレーションシップを、
ADO.NET でどのようにコード化するかを見てみましょう。
DataColumn dcCustomerCustID, dcOrdersCustID;
// 2 つの DataColumn オブジェクトにデータを設定します。
dcCustomerCustID = DataSet1.Tables["Customers"].Columns["CustID"];
dcOrdersCustID = DataSet1.Tables["Orders"].Columns["CustID"];
// 2 つの列のリレーションシップを作成します。
DataRelation relCustomerOrders;
relCustomerOrders = new DataRelation("CustomerOrders",
dcCustomerCustID, dcOrdersCustID);
新たに作成された DataRelation オブジェクトは、
DataSet に追加しなければあまり役立ちません。
DataSet1.Relations.Add(relCustomerOrders);
DataSet オブジェクトは Relations データ メンバを持っています。
このデータ メンバは、
DataSet のテーブルに関連するすべてのリレーションが保持される DataRelationCollection オブジェクトです。
同じ DataSet にある 2 つのテーブル内の一致する列の間には、
何らかのリレーションが作成されることに注意してください。
このため、列の .NET 型は同じである必要があります。
列の .NET 型は DataType プロパティによって返される値によって指定されます。
2 つのテーブル間に設定された親子リレーションシップがあるときに、
親テーブルで値を削除したり更新したりすると、
子テーブルの行に影響を与えることになります。
子テーブルの行への影響は以下のいずれかの方法で明示されます。
- 子テーブルの行には、連鎖削除または連鎖更新が行われる必要があります。
- 子テーブルの列の値が NULL 値に設定されます。
- 子テーブルの列の値が既定値に設定されます。
これを ForeignKeyConstraint ポリシーを使って直接管理しない場合は、操作が例外を生成します。
したがって、
変更する予定があるキャッシュされた非接続データに対してインメモリ リレーションを作成する場合は、
まず親テーブルで ForeignKeyConstraint オブジェクトを定義します。
この定義によって、関連するテーブルに影響するあらゆる変更が適切に管理されます。
ここで、次のような制約を作成します。
ForeignKeyConstraint fkc;
DataColumn dcCustomersCustID, dcOrdersCustID;
// 列を取得して制約を作成します。
dcCustomersCustID = DataSet1.Tables["Customers"].Columns["CustID"];
dcCustomersCustID = DataSet1.Tables["Orders"].Columns["CustID"];
fkc = new ForeignKeyConstraint("CustomersFK",
dcCustomersCustID, dcOrdersCustID);
// 削除と更新の制約をそれぞれ具体化します。
fkc.DeleteRule = Rule.SetNull;
fkc.UpdateRule = Rule.Cascade;
ForeignKeyConstraint は、親テーブルと子テーブルが共有する共通の列を使って親テーブルに作成されます。
親テーブルの列が削除または更新されるたびに子テーブルがどのように動作するかを指定するには、
DeleteRule フィールドと UpdateRule フィールドを使用します。
この場合は、親テーブルの行が削除されると、対応する子テーブルの行の値をすべて NULL に設定します。
さらに、すべて更新は親テーブルの行から子テーブルの行に単純に少しずつ行われます。
DataTable は、
DataTable の Constraints プロパティを使ってアクセスできる ConstraintCollection クラスの
ForeignKeyConstraint オブジェクトのコレクションを管理します。
最後に、EnforceConstraints プロパティを false に設定すると、
制約がテーブルに適用されないことに注意してください。
// 制約を追加し、適用します
DataSet1.Tables["Customers"].Constraints.Add(fkc);
DataSet1.EnforceConstraints = true;
ADO.NET は DataRelation オブジェクトが有効に作成できるかどうかを作成時に確認します。
基本的には、関連するすべての列が実際に指定されたテーブルの一部であるかどうかを確認します。
構文によっては、実際には実行中に作成した "適切な"列ではなく、
適切な 型と名前を持つ DataColumn オブジェクトを DataRelation のコンストラクタ渡すことができます。
DataRelation オブジェクトとそれに関連する DataTable オブジェクトは、
そのリレーションが DataSet の Relations コレクションに追加されるまでは、
分離独立したオブジェクトです。
この場合、ADO.NET はテーブルへのリレーションが無効になるかもしれないすべての変更を禁止します。
たとえば、ある DataSet から別の DataSet にテーブルを移動することだけでなく、
列への変更も禁止されます。
また、DataRelation オブジェクトは、
それを DataSet に追加する前に、
リレーションの有効性を確認できる CheckStateForProperty と呼ばれるメソッドを持っています。
このメソッドで行われる制御には、
親テーブルと子テーブルが異なる DataSet オブジェクトに属しているかどうか、
列の型が一致しているかどうかなどの確認が含まれており、
親テーブルの列と子テーブルの列が同一の列ではないことを確認します。
DataRelation が DataSet にまだ所属していない場合、
つまり関連テーブルが属している DataSet にまだ所属していない場合、このメッソドを呼び出せます。CheckStateForProperty は成功か失敗かを意味するブール値を返しません。エラーの場合は、 DataException 例外を使って通知されます。
子レコードにアクセスする
親と子のデータのリレーションを与えらたとすると、
親の行に関連付けらた子の行にどのようにアクセスできるでしょうか ?
つまり、DataSet 内に同じ Customers テーブルと Orders テーブルがあると想定すると、
どうやって指定した Customers の行に対する受注を取得できるのでしょうか ?
関連データにアクセスするコードでは、
まず親の行を表している DataRow オブジェクトを取得します。
これを行う方法は数多くありますが、使用する方法はアプリケーションの構造によってまったく異なります。
たとえば、行を一意に識別する主キーの値がわかっている場合、
そのテーブルの行を表す RowsCollection オブジェクトの Find メソッドを使用できます。
DataRow r = DataSet1.Tables["Customers"].Rows.Find(nCustID);
正しい DataRow オブジェクトを保持した後では、
指定したリレーションに従って子の行を取得することは、
メソッド GetChildRows を呼び出して DataRow オブジェクトの配列にデータを設定するのと同じくらい簡単です。
DataRow[] rgCustomerOrders;
rgCustomerOrders = r.GetChildRows(relCustomerOrders);
GetChildRows は、
DataSet に設定された有効な DataRelation オブジェクトへの参照である 1 つの引数を受け取ります。
このメソッドは、子テーブルの行を DataRow オブジェクトの配列として返します。
次のコードは指定した得意先の受注すべてをコンソールにダンプする方法を示しています。
for (int i=0; i < rgCustomerOrders.Length; i++) {
DataRow tmp = rgCustomerOrders[i];
Console.WriteLine(tmp["CustID"].ToString());
Console.WriteLine(tmp["Date"].ToString());
Console.WriteLine(tmp["ShipAddress"].ToString());
Console.WriteLine("");
}
正直に言うと、GetChildRows はいくつか別のプロトタイプを使って呼び出すことができます。
確かに上記で示した方法で DataRelation オブジェクトを指定することはできます。
しかし、名前によってリレーションを示すこともできます。
rgCustomerOrders = r.GetChildRows("CustomerOrders");
さらに、返す必要のある行をさまざまな形式で選択できます。
これは以下のシグネチャを使って行います。
public DataRow[] GetChildRows(
DataRelation relation,
DataRowVersion version
);
public DataRow[] GetChildRows(
String relationName,
DataRowVersion version
);
行の形式は DataRowVersion 列挙値を使って示します。
使える値は Default、Original、Current、および Proposed です。
自動マスタ/詳細ビュー
DataRelation オブジェクトは、
ある DataTable オブジェクト内の列を別の DataTable オブジェクト内の列に関連付けるので、
DataTable はそれ自体がマスタ/詳細ビューの構築に非常に役立ちます。GetChildRows メソッドは、そのようなビューを構築するための重要なツールです。
この動作が非常にすばらしいことを理解すると、
それ以上に画期的な Windows フォーム DataGrid コントロールが大好きになるでしょう。
DataSource プロパティで指定されたソースからのデータを表示するように、
DataGrid コントロールを設定します。DataSource が DataSet や DataViewManager などのコンテナ コントロールを指すようにすると、
子テーブルごとに 1 行、先頭に + 記号を付けて表示します。
この記号をクリックするとそのテーブルのコンテンツが表示されます。
また、子テーブルの名前を持つ DataMember プロパティを設定して、
特定のテーブルを選択できます。
theMasterGrid.DataSource = ds
theMasterGrid.DataMember = "Customers"
フォームに 2 つの DataGrid コントロールがあり、
マスタ/詳細ビューを実現したい場合、各グリッドを異なるテーブルに関連付け、
マスタ テーブルで新しい項目が選択されたときに発生するイベントに結び付けることができます。
この時点で、関連する子テーブルの行を含む配列にアクセスでき、
実行中に DataTable を作成し、
詳細 DataGrid の DataSource プロパティを更新できます。
実行時に SetDataBinding メソッドを使って、
Windows フォーム DataGrid の DataSource プロパティをリセットする必要があることに注意してください。
このアプローチは正しく機能しますが、
DataGrid コントロールはもっと適切にこの動作を行うことができます。DataMember プロパティの設定時に特別な構文を使うと、
DataGrid が詳細ビューを自動的に更新できます。
theChildGrid.DataSource = ds
theChildGrid.DataMember = "Customers.CustomerOrders"
親テーブルの名前と既存のリレーションの名前を連結し、
この 2 つの中間にドット記号 (.) を入れると、
DataGrid コントロールは Customers テーブルで現在選択している行に、
CustomerOrders リレーションに対して GetChildRows を自動的かつ暗黙に呼び出すようになります。
DataGrid によって行われるマジックは、これで終わりではありません。
2 つのグリッドが同じデータ ソースを持っている限り、
子のグリッドは、新しい行がマスタ グリッドで選択されたことを示すイベントに自動的に連結されます。
結局、1 つのリレーション、つまり 2 つの Windows フォーム DataGrid と次の 4 行のコードがあれば、
自由に自動更新するマスタ/詳細ビューを作成することができます。
theMasterGrid.DataSource = ds
theMasterGrid.DataMember = "Customers"
theChildGrid.DataSource = ds
theChildGrid.DataMember = "Customers.CustomerOrders"
ここで生じる疑問は、
"どうやって子グリッドが親グリッドについて知ることができるのか ? " ということです。
基本的に、親子リレーションシップのコンテンツを割り当てられているすべての DataGrid は、
DataSource プロパティに同じコンテンツを持ち、
そのメンバ式の最初の部分 (上記の例の Customers) と一致する DataMember を持つ、
同じフォームで実行中のインスタンスの別のグリッド オブジェクトを調査します。
XML で DataRelations をレンダリングする
リレーションは、DataSet オブジェクトの重要な情報を構成します。
しかし、DataSet オブジェクトはいつでも一般的なリレーション表記から
XML に基づいた階層的な表記に切り替えることができます。
リレーションが設定されるときは、
内部的には DataSet オブジェクトは ADO のデータ シェイプ表記を使って行われること似た方法で機能します。
余分なフィールドが各行に追加され、
子テーブル内の子の行グループにリンクされます。
従来の表記から XML に切り替えると、この情報はどうなるでしょうか ?
これは 2 つの方法で行えます。
1 つは、DataSet に基づいた XmlDataDocument クラスの新しいインスタンスを作成する方法です。
XmlDataDocument xmlDoc = new XmlDataDocument(DataSet1);
もう 1 つは、WriteXml を使って、DataSet 全体を XML に保存する方法です。
どちらの場合でも、
Nested プロパティが DataRelation オブジェクトに保持している値によって、
取得する結果がかなり異なります。
既定では、Nested は false に設定されるブール値です。
これは、あるリレーションの子の行が XML でレンダリングされる方法を制御します。
2 つの DataSet テーブルは、次のようにレンダリングされます。
<CustomerOrders>
<Customers>
<CustID>1</CustID>
<Name>Acme Inc</Name>
</Customers>
<Customers>
<CustID>2</CustID>
<Name>Foo Corp</Name>
</Customers>
<Orders>
<CustID>1</CustID>
<Date>2000-09-25T00:00:00</Date>
</Orders>
<CustomerOrders>
各レコードは、テーブル名と、列数と同じ数のテキスト ノードを持つサブツリーでレンダリングされます。
Nested が false に設定されている限り、
リレーションが設定されていてもこの表記は変更されません。
Nested を ture に設定すると、
指定された得意先のすべての受注ノードが子サブツリーとしてレンダリングされます。
<CustomerOrders>
<Customers>
<CustID>1</CustID>
<Orders>
<CustID>1</CustID>
<Date>2000-09-25T00:00:00</Date>
</Orders>
<Name>Acme Inc</Name>
</Customers>
<Customers>
<CustID>2</CustID>
<Name>Foo Corp</Name>
</Customers>
<CustomerOrders>
指定された得意先に対応するすべての受注は、
その得意先のノードの下に移動し、より合理的で使いやすい構造で構築されます。
まとめ
DataRelation は、
共通の列を使って 2 つのテーブル間の論理的なリンクを表す ADO.NET オブジェクトです。DataRelation は、親子リレーションシップを定義しますが、
テーブルと列は独立したエンティティのままです。
リレーションが設定された後は、
DataRow オブジェクトのメソッドを使用するか、
XML の階層的な表記に切り替えて、詳細テーブルの子の行に簡単にアクセスできます。
DataRelation オブジェクトは、重複した同じ情報を持たないことを除けば、
インメモリの INNER JOIN に似ています。
ダイアログ ボックス : ビューを使った変更
DataView オブジェクトの AllowXXX プロパティの役割は何でしょうか ?
DataTable のビューを使って行を変更できるのでしょうか ?
DataView は、
指定されたテーブルのコンテンツの特定の表記を提供するオブジェクトです。
DataView と DataTable は独立したオブジェクトで、
DataView は単に親テーブルへのリンクを保持します。
DataView は、テーブルをキャッシュすることも、
データの内部コピーを作成することも行いません。
DataView は、単にテーブルのコンテンツが表示される方法や順番に関するいくつかの情報を持つオブジェクトです。
DataView で実行する主要な機能は、
アイテムの列挙です。
これは、そのコンテンツをループするときは明示的に行われ、
データ連結コントロールの DataSource プロパティに DataView を割り当てるときは暗黙に行われます。
データ連結コントロールがその DataBind メソッドを呼び出すと、
データ ソースのコンテンツが列挙され、
そのコントロールの Items コレクションが正しく公開されます。
ビューが関連する場合、
呼び出し側はビューを使って列挙し、次に親テーブルを使って列挙して、
式やフィルタの並べ替えを適用します。
AllowEdit、AllowDelete、および AllowNew のブール値のプロパティは、
DataView とそれに関連付けられたユーザー インターフェイスが、
更新、削除、挿入を許可するかどうかを示します。
これは、親テーブルが更新される方法には影響しません。
これらのプロパティは、
DataView コントロールまたはそれを使用するデータ連結コントロールを使って実行する編集操作のみに適用します。
|
Dino Esposito は Wintellect に勤務していて、そこで ADO.NET と ASP.NET のトレーニングとコンサルティングを行っています。彼は VB-2-The-Max の共同設立者で、MSDN Magazine に Cutting Edge コラムを連載しています。
|