私は本ブログで、常々、DataSet+DataTableはダメダメすぎるので、Entity FrameworkでPOCOやるべき、と、述べている。しかしながら、業務アプリとなると、未だにこういう旧来の技術が使われる場合が多い。私のブログの、DataSet関連の記事には一定数のアクセスがいつもある。
そういう、旧来のダメフレームワークでプログラミングしなければならない悲しい人達(私含む)の一助となるのではないかと思い、DataTableの難解なモデルと操作方法をまとめてみたい。
値の設定、読み取り
DataTablesには、RowsプロパティとColumnsプロパティがあります。それぞれ、行の集合と列の集合を表します。値にアクセスするためには、必ず行→列という順序で辿らなくてはなりません。
良い例:
最初の行のNameという列にTaroという文字を入れます。
[csharp]
table.Rows[0]["Name"] = "Taro";
[/csharp]
ダメな例(コンパイルできない):
DataColumnにはインデクサプロパティが無いのでエラーになります。
[csharp]
table.Columns["Name"][0] = "Taro";
[/csharp]
行の状態
行には状態があります。DataRow.RowStateプロパティでアクセスできます。
Added:行が DataRowCollection に追加されましたが、AcceptChanges が呼び出されていません。
Deleted: DataRow の Delete メソッドを使用して行が削除されました。
Detached:行は作成されましたが、DataRowCollection の一部ではありません。 DataRow は、作成された直後からコレクションに追加されるまでの間、またはコレクションから削除された場合に、この状態になります。
Modified:行が変更されましたが、AcceptChanges が呼び出されていません。
Unchanged:行は AcceptChanges が最後に呼び出されてから変更されていません。
上記のように書いてありますが、初めて見る人にとっては意味がよくわからないと思います。以下、補足します。
DataRowは、ふつう、単体では存在しません。必ず、いずれかのDataTableのインスタンスによって所有されます。Detachedというのは、DataRowが作成されたきり、どのDataTableにも所属していない状態です。この状態を選択的に扱う場面は殆ど無いでしょう。
新しいDataRowがDataTableに追加されると、そのDataRowの状態はAddedになります。以降、DataRowの内容がどれだけ編集されようとも、Addedのままです。
DataTable.AcceptChanges()を実行すると、そのDataTableに含まれるすべての行の状態はUnchangedになります。DataAdapterを使用してDataTableを作成した場合も、すべての行の初期状態はUnchangedになっています。
Unchangedな行を編集する(列を指定して値を設定する)と、Modifiedになります。
Unchangedな行に対してDataRow.Delete()を呼び出すと、その行はDeletedになります。削除された行には、通常の方法ではアクセスできなくなりますが、データも、行そのもののインスタンスも残っています。
行の状態は、DataAdapterクラスで使用されます。DataAdapter.Update()を呼び出した時に、行の状態を読み取り、適宜delete, update, insertコマンドが実行されます。
LINQを使う
DataTableは、なんと、IEnumerable<DataRow>インタフェースを実装していません。なので、LINQが使えません。以上。終わり。
では、あまりに悲しいので、System.Data名前空間をインポートすると、AsEnumerable()という拡張メソッドが使えるようになります。
例:
[csharp]
var target = table.AsEnumerable().Where(r => r["status"].ToString() == "人間のクズ")
[/csharp]
DataTableのコピーを作る
DataTableには、Clone()とCopy()という2つのメソッドが有ります。
Clone()は、DataTableのスキーマ、つまり、同じDataColumnの集合を持つテーブルを作ります。ただし、列は一つもなく、空です。
Copy()は、列を含めた完全なディープコピーを作ります。
名前がわかりにくすぎると思うのですが、どうでしょうか。
他のテーブルから行をコピーする
DataTable.ImportRow()を使います。ただし、コピー元の行に含まれている列の行き先がちゃんとある(コピー先のスキーマがコピー元のスキーマと同じか、包含している)必要があります。
また、ImportRowを実行した時は、RowStateも引き継ぐようです。
すべての行をコピーしたいのであれば、Mergeメソッドが利用できます。
DataTableの行を絞込/ソートして新しいDataTableを作る
2つ方法があります。一つ目は、DataViewを使う方法です。
[csharp]
DataTable table = getDataTableSomeMethod();
DataView view = new DataView(table);
view.RowFilter = "name = 'さとう としお'";
view.Sort = "name = 'Age Asc, Name Asc'";
var newTable = view.ToTable();
[/csharp]
DataViewはその名の通り、DataTableを変更することなしに、絞り込みやソートを実現するクラスです。DataViewにはToTableというメソッドがあるため、現状の絞込状態をそのままテーブルとして出力させることが出来ます。
しかしながら、やり方がダサすぎます。なんですか?RowFilterプロパティに専用の書式で書いた文字列を設定するとかいうインタフェースは。人をおちょくっているのでしょうか。SQLライクな書式にしたから、頑張って覚えてね、とでも言いたいのでしょうか。なぜこいつ1人だけのために、専用の書式を覚えなければならないのでしょう。覚えなければ毎回MSDNを検索するはめになります。
なので、Linqで好きなように絞り込んで、その後でImportRowするやり方を私は好んで使います。また、絞り込み/ソート条件が動的に決まる場合は、Linqのほうがずっとやりやすいでしょう。
[csharp]
DataTable table = getDataTableSomeMethod();
var rows = table.AsEnumerable()
.Where(r => (string)r["Name"] == "さとう としお")
.OrderBy(r => r["Age"]);
var newTable = table.Clone();
foreach (var r in rows)
newTable.ImportRow(r);
[/csharp]
頻繁にこういうことをする場合は、IEnumerable<DataRow>の拡張メソッドでToTable()なんかを定義してやってもいいと思います。
状態を自在に変える
DataRowの状態を強制的にModifiedもしくはAddedにする場合には、SetAdded()、SetModified()というメソッドがあるのでこれを実行します。ただし、これが可能なのは元のDataRowがUnchangedであった場合のみです。
このような場合は、一度DataTableのAcceptChanges()を呼び出すしか方法はありません。
まとめ
思いつくままに幾つか書いていました。まだまだ忘れているだけで、他にもたくさんTips的なものはあると思います。また思い出したら追記してみます。
しかし、それより何より重要なことは、そもそも、複雑でわかりにくいライブラリを使うことそれ自体が問題です。Entity Frameworkという後継の技術があるのですから、それを使うことを初めに検討すべきでしょう。
私はいろいろあって、仕事でDataTableに付き合ってもはや2年になろうとしていますが、今思い返すと、DataAdapter/DataSet/DataTableの機能を自分で再実装したほうが便利だったとすら思います。
そして、ここまで苦労して覚えたとしても、DataTableには将来性も無ければ、他でつぶしが効く知識が身につくわけでもありません。さっさとみんなでWPF+Entity Frameworkに切り替えましょう。