C#

DataSetとDataTable

経験的に、C#ではDataSetやDataTableをうまく使いこなせるかというところが、効率的なプログラミングのひとつのポイントになっているようです。DataTableはさまざまなデータの内部データ構造として利用されます。また、画面上でもコントロールへのデータバインドによって細かいプログラミングなしで、簡単に表示と編集を行うことができるようになっています。DataSetはDataTableのコンテナとして利用できる他、XMLデータの入出力としても利用できます。このページにはDataSetとDataTableを扱う上でのヒントを記します。

削除された行へのアクセス

通常はDataTableの削除された行へのアクセスはできませんが、“何が削除されたのか”が重要なことがあり、削除された行を調べたいことがあります。

削除された行をまとめて取得する

データベース上のテーブルを更新するときなどで、元データを削除するためにDataTableの削除された行を取り出すときに重宝します。DataTable.GetChanges()メソッドは、該当行がない場合にnullを返してきますので、少し注意が必要です(*2)。そして取得したDataTableに対してDataTable.RejectChanges()を呼び出して行の状態を元に戻します(*3)。この動作をうまく利用するために、適切なタイミングでDataTable.AcceptChanges()を呼び出して、全ての変更を確定させておく必要があります(*1)。

    DataTable SourceTable;

    ...

    SourceTable.AcceptChanges();	// ... *1

    ...

    DataTable Deleted = SourceTable.GetChanges(DataRowState.Deleted);

    if (Deleted != null)		// ... *2
    {
	    Deleted.RejectChanges();	// ... *3
    }

    ...

削除された行、追加された行、更新された行をそれぞれ取り出して効率的にデータベースを更新することも可能です。

削除されたときに調べる

DataTable.RowDeletedイベントなどで、削除された行を逐次調べるような場合にはもうひと工夫必要です。削除された行を含むDataTableと同じスキーマの空テーブルを作り(*4)、削除された行をインポートします。つまり削除された行のコピーを作ります。そのあとでDataTable.RejectChanges()メソッドを呼び出して行の状態を元に戻します。そうすると、このコピーを通じて削除された行を調べることができます。

    DataTable SourceTable;

    ...

    SourceTable..RowDeleted += new DataRowChangeEventHandler(SourceTable_RowDeleted);

    ...

    void SourceTable_RowDeleted(object sender, DataRowChangeEventArgs e)
    {
	    DataTable Deleted = e.Row.Table.Clone();	// ... *4
	    Deleted.ImportRow(e.Row);
	    Deleted.RejectChanges();
	    DataRow DeletedRow = Deleted.Rows[0];

	    ...
    }

DataSetのシリアライズ

.NET Framework 2.0からバイナリ形式のシリアライズがサポートされましたが、.NET Framework 1.0や1.1では、DataSetのシリアライズは常にXML=テキスト形式です。2.0でウェブ・サービスを利用する場合にシリアライズ形式がどうなるのかは、まだ試していません。ここではテーブル名やカラム名およびカラム・マッピングがシリアライズに与える影響について考えてみます。

DataSetのシリアライズは次のコードとほぼ同じと考えて良いと思います。

    DataSet SourceSet;
    Stream OutputStream;

    ...

    SourceSet.WriteXml(OutputStream, XmlWriteMode.DiffGram | XmlWriteMode.WriteSchema);

BinaryFormatter.Serialize()メソッドを使ったとしても、型情報等を除いたデータ部分はXML形式になります。したがって全体としてはバイナリ形式ですが、内容のほとんどはXML=テキスト形式です。このストリームはだいたいスキーマ部分とデータ部分から構成されます。

この話題にはあまり重要ではないので、ここではスキーマ部分については取り上げません。気をつけなければいけないのはデータ部分のほうです。例えば次のようなDataSetがあるとします。

    DataSet SourceSet = new DataSet("MyDataSetName1");
    DataTable SourceTable = SourceSet.Tables.Add("MyDataTableName1");
    SourceTable.Columns.Add("MyDataColumnName1", typeof(string));
    SourceTable.Columns.Add("MyDataColumnName2", typeof(string));
    SourceTable.Rows.Add(new object[] {"row=0&cell=0", "row=0&cell=1"});
    SourceTable.Rows.Add(new object[] {"row=1&cell=0", ""});

このDataSetのデータ部分は次のようにシリアライズされます(XMLなので「&」はエンティティ参照「&」に置き換わります)。

    <MyDataSetName1>
	    <MyDataTableName1>
		    <MyDataColumnName1>row=0&amp;cell=0</MyDataColumnName1>
		    <MyDataColumnName2>row=0&amp;cell=1</MyDataColumnName2>
	    </MyDataTableName1>
	    <MyDataTableName1>
		    <MyDataColumnName1>row=1&amp;cell=0</MyDataColumnName1>
		    <MyDataColumnName2></MyDataColumnName2>
	    </MyDataTableName1>
    </MyDataSetName>

テーブル名とカラム名がデータ行1行につき2回ずつ出力されているのがわかるでしょうか。このため、長いテーブル名は一次関数的にシリアライズのサイズを増大させ、長いカラム名はさらにこの傾斜を大きくします。つまり、テーブル名とカラム名は短いほうが良いのです。テーブル名とカラム名を短くする次のコードを通すと、シリアライズのサイズを小さくできます。

    void CompressSerializeSize(DataSet DataSource)
    {
	    for (int TableIndex = 0; TableIndex < DataSource.Tables.Count; TableIndex++)
	    {
		    DataTable Table = DataSource.Tables[TableIndex];
		    Table.TableName = "T" + TableIndex.ToString("X2");

		    for (int ColumnIndex = 0; ColumnIndex = Table.Columns.Count; ColumnIndex++)
		    {
			    DataColumn Column = Table.Columns[ColumnIndex];
			    Column.ColumnName = "C" + ColumnIndex.ToString("X2");
		    }
	    }
    }
↓ ↓ ↓
    <MyDataSetName1>
	    <T00>
		    <C00>row=0&amp;cell=0</C00>
		    <C01>row=0&amp;cell=1</C01>
	    </T00>
	    <T00>
		    <C00>row=1&amp;cell=0</C00>
		    <C01></C01>
	    </T00>
    </MyDataSetName>

カラム名が重要である場合はキャプションに保存しておき、デシリアライズしたあとで復元するようにすると良いかもしれません。また、処理的に差し支えなければ、カラムのマッピングを要素から属性にするとさらに短くできます。

    void CompressSerializeSize(DataSet DataSource)
    {
	    for (int TableIndex = 0; TableIndex < DataSource.Tables.Count; TableIndex++)
	    {
		    DataTable Table = DataSource.Tables[TableIndex];
		    Table.TableName = "T" + TableIndex.ToString("X2");

		    for (int ColumnIndex = 0; ColumnIndex = Table.Columns.Count; ColumnIndex++)
		    {
			    DataColumn Column = Table.Columns[ColumnIndex];
			    Column.ColumnName = "C" + ColumnIndex.ToString("X2");
			    Column.ColumnMapping = MappingType.Attribute;	// ←ここです
		    }
	    }
    }
↓ ↓ ↓
    <MyDataSetName1>
	    <T00 C00="row=0&amp;cell=0" C01="row=0&amp;cell=1" />
	    <T00 C00="row=1&amp;cell=0" C01="" />
    </MyDataSetName>

それから、ひとつ覚えておきたいのは「DBNull」がシリアライズされないという点です。DBNullを許容する列で、必要のないデータにDBNullをセットしておくとシリアライズのサイズを小さくできます。

    SourceTable.Rows[1][1] = DBNull.Value;
↓ ↓ ↓
    <MyDataSetName1>
	    <T00 C00="row=0&amp;cell=0" C01="row=0&amp;cell=1" />
	    <T00 C00="row=1&amp;cell=0" />
    </MyDataSetName>

カラムのデフォルト値を設定してデータにデフォルト値を指定することでは、シリアライズを避けることはできません。

    SourceTable.Columns[1].DefaultValue = "";
    SourceTable.Rows[1][1] = "";	// → 空文字列としてシリアライズされる

シリアライズ前にデフォルト値を設定し、デフォルト値と同じ値をDBNullに置き換え、デシリアライズ後にDBNullをデフォルト値に戻すといった操作は考えられます。

シリアライズのサイズに対して配慮のない長いテーブル名と長いカラム名が利用される場合、シリアライズのサイズは最小のものに比べて数倍から10数倍にも膨れ上がります。巨大なデータを扱うシステムでは入出力の遅延を増長させ、ひいてはタイムアウトによるエラーを発生させかねません。

(2007/11/20 初稿)