Entity FrameworkでDBのデータをファイルに取得したり書き戻したりしたい

テストのためにデータを同じ状態に戻したい、とか、一部の本番環境データをステージング環境に持っていきたい、というときに、いままでは、DataSetに取得 → ファイルにシリアライズ → 移行先の、キーが重複するデータを削除 → デシリアライズしたオブジェクトをDBに投入、ということをやっていたのですが、同じことをEntity Frameworkでやるにはどうすればいいのか実験しました。
テストに使ったDB、ではなく、エンティティはこういうかんじ。生成に使ったのは、たまたま手元にあったDBです。
20090314145532
テーブルとエンティティは単純に1:1でマッピングしてます。外部キーが設定してあると勝手にアソシエーションをたどるプロパティを作って、相互に参照できるようにしてくれます。

とりあえず、Entityを表示したりしてみる

一件のOsiUnitをDBから取得、それに紐付くOsimenを取得し、コンソールに表示してみます。

using (hpmaEntities e = new hpmaEntities())
{
    OsiUnit unit = e.OsiUnit.Where(u => u.OsiUnitCode == 9).First();
    Console.WriteLine("UNIT:" + unit.OsiUnitName);

    unit.OsiMen.Load();

    foreach (OsiMen men in unit.OsiMen) Console.WriteLine(" Member:" + men.OsiMenName);
}

実行結果。

UNIT:℃-ute
 Member:梅田えりか
 Member:矢島舞美
 Member:村上愛
 Member:中島早貴
 Member:鈴木愛理
 Member:岡井千聖
 Member:萩原舞
 Member:有原栞菜

hpmaEntitiesは自動で生成されるクラスで、System.Data.Objects.ObjectContextを継承してます。物理的には、DBへの接続なのかなーと思うので、hpmaContextとかって名前になってくれるほうがしっくりくるかなーと思いますが、まぁ自分で直せばいいだけです。
あと、意外だったのは、unit.OsiMen.Load();などと明示的に指定しないと、アソシエーションの先のデータは取ってきてくれないことです。

"隠されたネットワーク ラウンドトリップの排除" という原則に基づき、Entity Framework では自動遅延読み込みはサポートされません。コードで生成したクラスから構成されたグラフをトラバースするという単独の動作では、ストアのクエリがトリガされることはありません。したがって、使用するオブジェクトはユーザー コードで明示的に取り込む必要があります。

だそうです。Include というクエリビルダメソッドを使えば一括で読み込めるということも書いてありますが、クエリビルダメソッドがなんなのかわからない(このページではほかに一回も出てこない)ので今回は見送りました。

Entityをシリアライズする

Entityのシリアライズに関してはオブジェクトのシリアル化 (Entity Framework)に書いてあります。バイナリフォーマッターを使ったシリアライズに関しては、ほかに専用の解説ページ、オブジェクトをシリアル化およびシリアル化解除する方法 (Entity Framework)があったのですが、XMLで中を見てみたかったので、XMLシリアライズにしました。

public static string Serialize<T>(T value)
{
    StringWriter sw = new StringWriter();
    new XmlSerializer(typeof(T)).Serialize(sw, value);
    return sw.ToString();
}

public static T Deserialize<T>(string value)
{
    return (T)(new XmlSerializer(typeof(T))).Deserialize(new StringReader(value));
}

超適当ですが、これで文字列にできたので、あとはファイルに書き込みます。stringにシリアライズしているので、ファイルはutf-16で書く必要があります。最初の引数はファイル名。

// シリアライズするとき
File.AppendAllText(UNIT_FILE, Serialize(unit), Encoding.GetEncoding("utf-16"));
File.AppendAllText(MEN_FILE, Serialize(unit.OsiMen), Encoding.GetEncoding("utf-16"));
// デシリアライズするとき
private static OsiUnit GetFromFile()
{
    OsiUnit ou = Deserialize<OsiUnit>(File.ReadAllText(UNIT_FILE));
    ou.OsiMen = Deserialize<EntityCollection<OsiMen>>(File.ReadAllText(MEN_FILE));
    return ou;
}

EntityをDBから削除

ファイルに出力したEntityを書き戻す前に、DBから削除しておく必要があります。デシリアライズしたオブジェクトとキーが重複してしまうからです。Entity SQLにはDML(Insert, Update, Delete)がありません、というのをみて、参照にしか使えねーのかよ!! とおもっていましたが、実際はContextに対して取得したEntityをつかって変更を行い、SaveChangesすれば、DBにたいして反映できます。オブジェクトの追加、変更、および削除 (Entity Framework)。こういう感じで、さっきシリアライズしたオブジェクトを削除できる。

using (hpmaEntities e = new hpmaEntities())
{
    OsiUnit unit = e.OsiUnit.Where(u => u.OsiUnitCode == 9).First();
    unit.OsiMen.Load();
    e.DeleteObject(unit);
    e.SaveChanges();
}

簡単。

シリアライズしたEntityをContextに戻してDBに反映する

ここが難しかった、参照したのは、オブジェクトの使用 (Entity Framework タスク)から辿れるページ群です。
まず、XMLからオブジェクトに戻すところは簡単にできたのですが、これをContextに適用するのがうまくいかない。

アタッチされたオブジェクトがデータ ソースに存在しない場合、そのオブジェクトは SaveChanges の実行時に追加されません。この場合、プロパティに変更が加えられると、SaveChanges の実行時にサーバーで例外が発生します。オブジェクトを追加するには、Attach の代わりに AddObject を使用します。

これを読む限りでは、もうデータソースであるDBからは消してしまっているので、AddObjectを使わなければならないっぽい。しかし、AddObjectを使うとInvalidOperationExceptionが出て、詳細は、"オブジェクトには既に EntityKey があるため、ObjectStateManager に追加できません。既存のキーを持つオブジェクトをアタッチするには、ObjectContext.Attach を使用してください。"。EntityKeyは設定できるんですが、OsiUnitのEntityKeyをnullにしても例外が変わらないのでダメやー、と30分くらい悩んでたんですが、アソシエーションの先のオブジェクトも全部EntityKeyをnullにしてあげれば大丈夫でした。
というわけで、こういう感じでDBに書き戻せました。

using (hpmaEntities e = new hpmaEntities())
{
    OsiUnit d = GetFromFile();
    d.EntityKey = null;
    foreach (OsiMen om in d.OsiMen) om.EntityKey = null;
    e.AddToOsiUnit(d);
    e.SaveChanges();
}