[C#]型付きDataSetは人格荒廃の元なのでおすすめしない話

アプリケーションからデータベースを扱う際、今だとEntity Frameworkを使うことがほとんどでしょうが、仕事ではまだまだDataTable/DataSetを使うケースも多いと思います。

VisualStudioのProfessional以上のエディション(たぶん)には、いわゆる「型付きデータセット」というものがあります。正式名称がよくわからないのですが、おそらく「厳密に型指定されたデータセット」が正しい名称だと思います。VisualStudioから「データセット」として追加して、既存のデータベースからスキーマをインポートすると厳密に型が指定されたデータセットができあげる・・・というあれです。まあ、ここでは一般的によくつかわれているっぽい「型付きデータセット」という名前で読んでみたいと思います。

この型付きデータセットがかなりのウンコ様です。メリットも大きいですがデメリットも同じくらい大きい。ウンコを使う理由などないので、今だと、Entity Frameworkが使えれば一番いいと思う。StackOverflowとかにも以下の様なQAがあった。

entity framework or dataset?

Entity Frameworkはまだ実行速度が遅いとか、プロバイダー周りがこなれてないとか、細かい問題がたくさんある。それでも、型付きDataSetを使う理由はほとんどない。以下にそう思う理由を列挙する。

1. 型付きDataSetはDBスキーマの変更に弱い

型付きデータセットのデザイナを開くと、サーバエクスプローラから読み込みたいテーブルをD&Dできる。すると自動的にスキーマ情報を解析し、DateTimeとかStringとかの型になっているプロパティを通じて、データベース中のレコードにアクセスできる。

この型付きのプロパティは、VisualStudioが裏でコードを自動生成することで実現している。しかし、自動生成するのは最初の1回のみで、あとでデータベースが変更されたときに自動で追従させる方法がない。無いので、デザイナーの画面からテーブルを削除して、再度自動生成させるしかない。

ここで問題なのが、エディタ上でなにか気の利いたSQL文を定義していたり、値がNullだった場合の動作を変えていたり、型のマッピングをちょっと変えていたりすると、それらはまた再設定しなければならない。これがすごく手間だ。

じゃあ、変更したスキーマに合わせて手動で設定すればいいんじゃないの?と、思うだろう。エディタ上に表示されたテーブルの定義は手動でも変更できる。DBを変更した後に、型付きデータセットの定義も変えてやればいい。そう思うだろう。しかし、このやり方で私は何度も痛い目を見た。

たとえば、主キー列を変更したとする。DataSetデザイナからは主キー列の設定がGUIで行える。だが、ここで設定した変更は、型付きDataSetに対応するDataTable中のUpdate、InsertなどのSQLCommand周りまでは反映してくれない。この結果、Update()メソッドを呼んだ時に、値が妥当かどうかチェックしているらしく、「主キーが空です」などのエラーメッセージが表示される。イライラする。イライラすると血圧が上がる。

いやいや、頻繁にデータベーススキーマを変えるようなプロジェクト運営している方がダメでしょ…と言いたくなるひとも多いだろう。でも、現実的にそういう理想的なプロジェクト運営をしようと努力しても、いつも馬鹿な客が無茶苦茶なことを要求してきて、交渉力のないプロジェクトリーダーがそれを安請け合いする、という構図がすべてを台無しにする、というのは誰しもが経験することだろう。

これも、デザインパターンと呼んだら面白いかもしれない。Volatile Specパターン。すなわち、頻繁に要求仕様を変更し、コミュニケーションを麻痺させることでプロジェクトメンバーをDeath Marchに参加させるパターン。

2. デザイナが重い

デザイナを起動するまでも重いし、起動してからも重い。
このため、1.で説明したように、プロパティを変更したり、型付きDataTableを再生成したりする作業そのものが苦痛。

3. 大量のコードを吐き出す

これは場合によりけりだ。まともなDB設計であれば、それほど問題にならないかもしれない。

しかし、1テーブルの列数が200列~300列、そのようなテーブルが30ほどあるという、二日酔いウンコ設計というのは、残念ながらこの業界では珍しいことではないだろう。するとデザイナが生成するコードも半端なく多かった。

上記のようなテーブルをすべて一つのDataSetにまとめてしまったおちゃめさんが我が社に居たんだが、そのようなDataSetから生成されたコードはなんと20万行もあった。そのようなデータセットが機能別に2個も3個もあった。

なので、DataSetを開く、コンパイルする、などをするたび頻繁にOutOfMemoryExceptionが発生する。デザイナを開いている時に発生したら最悪だ。吐き出される自動生成コードのファイル名に「1」がついたりして、同じクラスが2つ作られてコンパイルできなくなったりする。プロジェクトファイルをエディタで開いて熱心に直していけば元通りになるのだが、そんな気力もないのでまたソース管理ソフトに保存された最新ソースに戻すハメになる。

4. Nullの扱いが微妙

DB上の値がnullだった場合、型付きDataSetからこれを読んだり書いたりするのがすごく面倒。

具体例を示そう。いま、Personテーブルがあって、そこに職業コードを表すJobCodeというフィールドがあったとしよう。JobCodeは数値型で、C#のコードではintとして扱いたい。

で、ヒキニートの場合は、ヒキニートというJobCodeを定義したくなかったとする。何故ならば、あなたはヒキニートには人権が無いと思っているからだ。ヒキニートのためにわざわざヒキニートを表す職業コードを定義してやるのはまっぴらごめんである。そもそもニートは職業ではない。だから、どうしてもヒキニートのクソ野郎をPersonテーブルのレコードとして表現しなければならないとき、JobCodeはnullとするように取り決めた。

これを型付きデータセットに入れて、ヒキニートだけを抽出してゴミ溜めにブチ込むというコードを書きたいと思ったとする。すなわち、

HashSet<PersonRow> stinkGabage = new HashSet<PersonRow>();
PersonTable.Where(r => r.JobCode == null).Select(r => stinkGabage.Add(r));

このようなコードを書きたくなる。だが、これはうまくいかない。何故ならば、JobCodeプロパティはNullableな型になっていない(つまり、Nullableやint?でなくint型)からである。

なので、正しくは以下のように記述しなければならない。

HashSet<PersonRow> stinkGabage = new HashSet<PersonRow>();
PersonTable.Where(r => r.IsJobCodeNull()).Select(r => stinkGabage.Add(r));

IsJobCodeNull()メソッドは、デザイナが自動生成するコードである。もちろん、SetJobCodeNull()メソッドも対になって生成されるよ。全てのNull許容なフィールドに対して。こいつはダサすぎる。こんなもん使いたくない。

なので、あなたは「何であたしがこんなダサいコードを書かなきゃならないのよ、もっと他にやり方があるはずよ、見てらっしゃい」とDataSetのデザイナの画面を開き、列のプロパテイを調べる。

そうすると、NullValueというプロパティがあり、説明を見ると「この列がNullの場合に返される値です」となっている。デフォルトの設定は(ThrowException)となっており、また、選択肢を確認すると、(null)という値がある。

じゃあこれを(null)にしたらいいんじゃない??と思うはずだ。で、設定するだろう。すると、「入力された値が、現在のデータ型で有効ではありません。」と怒られる。

で、データ型を確認すると、intとかdecimalになっていることだろう。じゃあデータがをNullable<>な型にすればいいのねん。と、DataTypeプロパティの設定値を開くと、そこにはint?とかdecimal?なんて気の利いた型は無いのだ。

じゃあ(null)ってなんなのよ、と色々いじってみると、これはどうもStringとかObjectとかいうデータ型にしか設定できないということが分かってくる。「型付きデータセット」なのにObject型にするとか意味がわからない。本末転倒でもう何をやりたいかすら分からなくなってくる。近年の北朝鮮を思わせる無慈悲な仕様である。

NULL 値の処理(MSDN)

Nullable<T> または Nullable 構造体は、現在 DataSet ではサポートされていません。

一体全体、これなんなの?っていうか、なんで私がゴミ溜めみたいなプログラムを書かなきゃいけないの、一流大学を出たのに。ほんと、ムカつく!と、あなたはディスプレイに正拳突きを食らわせた後、プロジェクトリーダーをDATテープでぐるぐる巻にしてDVD-Rをフリスビーのように投げまくり、全てのLANケーブルを引きちぎってミノムシのように丸まって日がなサーバールームの片隅に巣食い、いずれは自分がヒキニートになるという暗い将来が待っています。

5. そもそも古い

型付きデータセットというか、そもそもDataTable/DataSetが古い技術だ。その設計思想は、Windows Formsのバインディング機構と関係性が深い。いずれ無くなってしまう可能性は高いと思われる。つうか絶対無くなる。こんなもん失敗作だ。

Windowsデスクトップで動作するプログラムは、WPF/Entity Frameworkが中心となりつつある。Windows FormsやDataTableの機能が大幅にバージョンアップされることはまず無く、今後はMFCやVB6のようにほそぼそと維持管理されることになるのだろう。

いずれ無くなってしまう技術を採用することは出来れば避けたい。

でも、実際のプロジェクトではそうも行かないことが多々ある。プロジェクトリーダーから、「過去の資産を有効活用しろ」などと、莫大な金をかけて作ったゴミプログラムを渡されたり。プロジェクトメンバのスキルの問題だったり。

って感じで、思いつく理由を6つ挙げてみました。

型付きデータセットの代替方法としては、以下の様なものが見つかります。
山本大@クロノスの日記 
型なしDataSetと型付DataSet、そして片想いORマッピング的な何か。 

型付きデータセットの最大のメリットとは当然ながら、型を厳密に扱える、二番目に、Intellisenseで列名が自動補完される、という事に尽きるでしょう。それさえ実現できれば、型付きデータセットを使わずとも目的が果たせそうです。

上記の方法はいずれも、DataRowを自作したクラスにマッピングする方法です。列の一つ一つが、ひとつのプロパティと対応します。

具体例を使って説明します。いま、Personテーブルがあり、PersonId, Name, Age, JobCode列があったとしましょう。これを「自作したクラスにマッピングする」とは、つまり、以下の様なクラス

public class Person
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public int? PersonId { get; set; }
}

を作成し、これに値を自動的に設定するという仕組みを構築する、ということです。Java使いな人であれば、「POJOオブジェクト」とか「EJB」とか言えばイメージがつくでしょうか。こういうことをやったのがEntityFrameworkです。

でも、これも完全な解決策では無いように思えます。

そもそも、型付きデータセットを使うという利点は、こういうマッパークラスを自分で作らなくても良い、という利点があるからです。IntelliSenseが効くというのはつまり、良い感じのマッパークラスがあるということです。

これが正規化されたごく少数の列で綺麗に表現されるスキーマだったらまだ我慢できますが、(何度も書くのは嫌ですが)現実はそうでは無いことのほうが多いです。

つまり、手作業でPOJO/POCOを作るコストが許容できないということです。

じゃあどうすればいいか…。

実は私も上記と同じようなことを考えました。ジェネリクスを使って、実際のテーブルに対応するマッパークラスでDataRowをラッピングしてやればいいと。Windows FormsとのBindingを使う必要も有りましたので、INotifyPeopertyChangedも実装します。で、マッパークラスは簡単なツールを作って自動生成させます。型付きデータセットデザイナの簡易版を再発明するみたいなもんですね。

でもこれも途中でやめました。「簡単なツールを作って自動生成させます」とさらっと書きましたが、考えていくとこれが結構面倒だからです。

じゃあどうすればいいか?残念ながら簡単な解決策はありません。対象となるプロジェクトごとに、出来るだけ自動化していくほうこうにするしかないでしょう。

まとめ

型付きデータセットはクズな上に代替方法がありません。

DataSet/DataTableより新しい次世代の技術がEntity Framework(EF)であり、WPFの高度なバインディング機構です。厳密な型指定がされていようと無かろうと、DataSet/DataTableは古い技術なので出来れば使用しない方が良いです。

ちなみに、EFとWindows Formsの組み合わせで作るのは不可能ではないですが、面倒だと思います。なぜなら、EFそれ自体には値の変更をUIに通知する機能(INotifyPropertyChanged)がないですし、Windows Formsでよく使われるBinding SourceもEFには対応していない(IBindingList, IBindingListViewなどを実装していない)からです。

すると、Formsのバインディング機構とEFの間にもう一つ、なにかそのミスマッチを解消するためのレイヤを差し込まなければなりませんが、そこまでするんだったら、私なら型なしDataTable/DataSetを素直に使ってしまいます。

・出来る限りEF+WPFで作る
・どうしてもWindows Formsで作る必要があれば型なしDataTableを使う、必要に応じてもうちょっと楽に実装できる方法を考える。
・型付きDataSetを使うときは、よくよくデメリットを考慮して使う。

って感じですかね・・・。