atsukanrockのブログ

Microsoft系技術を中心にぼちぼち更新します

データセットデザイナの機能と注意点

はじめに

データセットデザイナは、Visual Studio(バージョンによらず)の目玉機能のひとつだろう。GUI操作によって、型付けされたデータセット(以降「型付データセット」と呼ぶ)のソースコードを自動生成することができる。本エントリでは、Visual Studio 2005でのデータセットデザイナの機能を紹介し、使用上の注意点を挙げる。
なお、本エントリで使用する用語は、「データセット デザイナ」に倣う。

【機能】SQLを基に、データ格納用クラス(DataTable)およびデータアクセス用クラス(TableAdapter)を生成

手順1 [TableAdapter 構成ウィザード]の表示

データセットデザイナ上で何も選択せず右クリック→[追加]→[TableAdapter]とすると、[TableAdapter 構成ウィザード]が表示される。

手順2 SQLの入力

[TableAdapter 構成ウィザード]を進めると、SQLを入力するステップが表示される。"SELECT * FROM (<テーブル名>|<ビュー名>)"と入力して[クエリ ビルダ]を表示→[OK]を押下すると、実際にDBに接続して"*"の部分を明示的な列指定に変換してくれる。ただし[クエリ ビルダ]では、テーブル名やビュー名に(Oracleで言えば)スキーマ名が付いてしまうことに注意する。

手順3 DataTable作成完了

ウィザードを完了まで進めると、DataTableの作成が完了する。

サンプル

以下のようなコードが記述できるようになる。

// HOGEテーブルの全レコードを取得
HOGEDataTable dtHOGE = new HOGETableAdapter().GetData();
foreach (HOGERow drHOGE in dtHOGE)
{
    // 1行ごとの処理
}

【機能】DataTable内を主キーで検索するメソッドを生成

主キー(鍵マーク)について

データセットデザイナ上でDataColumn上に鍵マークが表示されていると、DataTableの主キーがその列であることを表す。鍵マークが複数の列に表示されている場合、DataTableの主キーが複合キーであることを表す。
主キー列には、DataTableにその列での一意検索メソッド(および一意制約(UniqueConstraint))が生成されている。一意検索メソッドは、型付けされたDataRowCollection.Findメソッドと呼べるものであり、シグネチャは以下のとおり。

メソッド名
「FindBy<列名(DataColumn.ColumnNameプロパティ値)...>」
引数
主キー列(複合キーの場合複数)
戻り値
(型付けされた)DataRow。見つからない場合null
サンプル

以下のようなコードが記述できるようになる。

// HOGEテーブルの全レコードを取得し、主キーであるHOGE_CD列値が"hoge"の行を検索
HOGERow drHOGE = new HOGETableAdapter().GetData().FindByHOGE_CD("hoge");
if (drHOGE != null)
{
    // データ行が見つかった場合の処理
}
else
{
    // データ行が見つからなかった場合の処理
}
主キーの手動設定

[TableAdapter 構成ウィザード]で入力したSELECT文が、複雑だったりビューを検索するものだったりすると、DataTableを作成しただけでは主キーが設定されない。
主キーを手動設定するには、DataTable上でキー列を選択し、右クリック→[主キーの設定]を押下する。複合キーの設定のためにキー列は複数選択できるが、選択する順序が意味を持つ。例えば、一意検索メソッドのメソッド名の列名部分は、選択した順に列名が並ぶ*1
設定した主キーの内容を確認したり変更したりするには、主キー設定した列を選択し、右クリック→[キーの編集]を押下する

【機能】リレーションシップの親レコードから子レコードを取得するメソッドを生成

手順1 [リレーションシップ]ダイアログの表示

データセットデザイナ上で何も選択せず右クリック→[追加]→[Relation]を押下すると、[リレーションシップ]ダイアログが表示される。

手順2 リレーションシップの作成

親テーブル、子テーブルおよびそのリレーションシップを構成する列を選択し、[OK]を押下すると、リレーションシップが作成される。

生成されるメソッドについて

リレーションシップを作成すると、親テーブルの1レコードを基に、それに紐づく子テーブルの複数(0...N)レコードを検索するメソッドが生成される。生成されたメソッドは、型付けされたDataRow.GetChildRowsメソッドと呼べるものであり、シグネチャは以下のとおり。

メソッド名
Get<子テーブル名>Rows
引数
なし
戻り値
子テーブルの型付けされたDataRowの配列。1レコードもない場合、要素数0の配列
サンプル

以下のようなコードが記述できるようになる。例えばASP.NETだと、Repeater.ItemDataBoundGridView.RowDataBoundなどのイベント内で、「たった今データバインドされたデータ行」の子レコードを取得するという使い方ができる。

// HOGEFUGADataSetには、HOGEDataTableとFUGADataTableの2DataTableを作成済
HOGEFUGADataSet ds = new HOGEFUGADataSet();
new HOGETableAdapter().Fill(ds.HOGEDataTable);
new FUGATableAdapter().Fill(ds.FUGADataTable);

foreach (HOGERow drHOGE in ds.HOGEDataTable)
{
    // drHOGEの子であるFUGARowの配列を取得
    FUGARow[] adrFUGAs = drHOGE.GetFUGARows();

    // FUGARowの配列に対する処理
}

【注意点】NULLの扱い

AllowDBNullプロパティについて

DataColumn.AllowDBNullプロパティが、DBにおけるNOT NULL制約を表す。
データセットデザイナ上でこのプロパティを設定するには、DataTable上の列を選択し、[プロパティ ウィンドウ]上で設定する。

自動生成されるコードについて

DataColumnのAllowDBNull設定が、自動生成されるコードに与える影響は以下のとおり。

  • AllowDBNullがFalseの列に、DB検索で取得したデータがNULLだった場合、取得した(Fillした)時点で例外が発生。Trueだと例外とならない
  • AllowDBNullがTrueの場合、その列の値がNULLかどうかを確認するIs<列名>Nullメソッド(型付けされたDataRow.IsNullメソッドと呼べるものである)が生成される。Falseだと生成されない
注意点

特に、データを取得した時点で例外が発生する仕様に注意する。この例外は、実行時に初めて発生する例外であり、ビルド時には表面化しない。実行時に当該列の値がNULLのデータを取得した場合に初めて表面化する。かなり発見しづらいバグとなり得るため、開発者は常に細心の注意を払うべき。
ちなみに、型付けされたDataRowから列の値を取得するコードでは、値がNULL(DBNull.Value)の場合例外が発生する。値がNULLである可能性がある列は、以下のようにしなければならない。

  1. AllowDBNullをTrueにする
  2. 型付けされたDataRowからの列値の取得前にIs<列名>Nullメソッドを呼び出し、値がNULL(戻り値がtrue)かどうかで分岐するコードを記述する
サンプル

AllowDBNullがFalseの列にNULL値が設定されると例外が発生する。

// HOGE_NAME列のAllowDBNullをFalseに設定しているとする
// DBにHOGE_NAME列値がNULLの行があると、以下のGetDataメソッド内で例外が発生
HOGEDataTable dtHOGE = new HOGETableAdapter().GetData(); // HOGEテーブルの全レコードを取得したい...

値の取得前にNULLかどうかをチェックしなければならない。

// HOGE_NAME列のAllowDBNullをTrueに設定しているとする
if (!drHOGE.IsHOGE_NAMENull())
{
    // HOGE_NAME値がNULLでない場合のみ取得する
    String sHOGE_NAME = drHOGE.HOGE_NAME;
}
else
{
    // HOGE_NAME値がNULLの場合、値を取得すると例外が発生
    // String sHOGE_NAME = drHOGE.HOGE_NAME; // 例外が発生するコード
}

【注意点】CASE式に弱い

どう弱いか

少なくともプロバイダがSystem.Data.OracleClientの場合、データセットデザイナのSQL解析は、CASE式を正しく解析できない。「クエリ テキストを解析できません。」という旨の警告メッセージが表示され、クエリに対応するメソッドの引数を正しく構築できない。

CASE式を使うための手順

xsdファイルをテキストエディタで編集するという方法がある。手順は以下のとおり。

  1. [TableAdapter 構成ウィザード]や[TableAdapter クエリの構成ウィザード]ではCASE式を使わないようSQLを記述し、TableAdapterのクエリを保存 ※列の名称、数はCASE式を使う場合と同じにしておく
  2. xsdファイルをテキストエディタで開き、CASE式を使うSQLに変更
  3. [ソリューション エクスプローラ]上でxsdファイルを右クリック→[カスタム ツールの実行]を押下(xsdファイルに記述したSQLが、自動生成コードに反映される)

以上の手順を踏めば、CASE式を使ったSQLを発行するクエリを、TableAdapterに追加できる。

【注意点】TableAdapterの再構成が危険

作成済みのTableAdapterのメインクエリに対し右クリック→[構成]を押下すると、[TableAdapter 構成ウィザード]を再実行することができる)。しかし、この操作には後述の危険が伴う。
これらの危険を回避するには、TableAdapterの再構成を行わないのが一番簡単だが、以下のような目的がある場合は、これらの危険を把握した上でTableAdapterの再構成を行った方が良いこともある。

  • DataTableの列を、クエリに合わせて追加・削除したい
  • DataTableの列定義を、DBから取得し直したい
危険1 意図的に設定した列プロパティが上書きされる

作成済みのTableAdapterに対する[TableAdapter 構成ウィザード]を完了まで持っていくと、DataColumn.AllowDBNull(前述の「【注意点】NULLの扱い」を参照)をはじめとする各プロパティの値が、上書きされる*2。AllowDBNullに自動設定されるのと異なる値を意図的に設定している場合などに、上書きされると困る。対策としては、以下が挙げられる。

方法 欠点
[TableAdapter 構成ウィザード]を使わず、[プロパティ ウィンドウ]でCommandTextプロパティを編集する SQLが勝手に整形されるため、を自分好みに整形できない。また、列の追加、削除に対応できない
前述のCASE式を使う方法で示した、xsdファイル直接編集→カスタム ツールの実行で変更する 列の追加、削除に対応できない
[TableAdapter 構成ウィザード]を使うが、再度意図的なプロパティ設定をできるよう、自動で設定されるのと異なる値を設定したプロパティについて、コメントを記述しておく 手間がかかる
危険2 メインクエリ以外のクエリも自動的に更新される

TableAdapterにクエリを2つ以上作成していた場合、TableAdapterの再構成によりメインクエリ以外のクエリ(以降「サブクエリ」と呼ぶ)も自動的に更新される。
この自動更新の内容が適切であればよいのだが、以下のような問題があるクエリとなってしまう。

  • テーブル名の前にスキーマ名が付く(後述の「クエリ ビルダを使うと、テーブル名の前にスキーマ名が付く」と同じ)
  • 改行、インデントされていない

クエリが2つ以上のTableAdapterで、どうしてもTableAdapterの再構成を行いたい場合には、[TableAdapter の構成ウィザード]によって自動更新されてしまったサブクエリを1つ1つ修正し、上記の問題がないクエリとする。なお、サブクエリに対し[構成]を実行すると[TableAdapter クエリの構成ウィザード]が表示されるが、これは[TableAdapter の構成ウィザード]とは異なり、別のクエリを自動更新するようなことはない。

【注意点】クエリ ビルダを使うと、テーブル名の前にスキーマ名が付く

[クエリ ビルダ]を使うと、テーブル名やビュー名の前にスキーマ名が付く。DBMSOracleの開発では、スキーマ名を指定しないで済むようにシノニムを作成するのが一般的だが、シノニムを作成していても上記のようになってしまう。[クエリ ビルダ]が生成したSQLから、スキーマ名を手作業で削除するしかない。

*1:DBのインデックスであれば列の順序が重要だが、.NETのDataTableだとどうなのかは未調査である

*2:AllowDBNull以外にどのプロパティが上書きされるかは未調査である