iPX の深津です。
先日 JR中央線の立川駅でケーブル火災がありまして、そのあおりで周辺の JR路線が軒並み運転見合わせになるという事故がありました。
復旧の見込みが全く立たないということでしたので職場から自宅まで歩いて帰ってきたんですが、まぁ、何とかなるもんですね。
もっとも日ごろの運動不足が祟り、しばらくは足が痛かったんですが。
ちょうどその日、 EntityFramework を使ったややこしいテストの話が出ました。
それを片付けてさぁ帰ろうとしたところで先の事故。
いやぁ、祟られてますなぁ。
テストの要件
さてその「ややこしいテスト」の要件は下記のとおりです。
- Oracle に対して読込は EntityFramework、書込は DataSet で行っているプログラムのテストをしたい。
- このテストをするために Oracle にテストデータを書込む必要があるが、テストが終了したらテストデータを削除しなければならない。
- このテストは MSTest によるユニットテストで行う。
テスト以前に、なんで EF と DataSet を混在させるようなややこしいことを。 え、 SQLServer には LINQ to SQL で読み書きしているですと? EF で統一しちゃえば……スミマセン、詳細は聞かないことにします。
TransactionScope はどう? ダメ?
えーと、テストメソッドの先頭で TransactionScope 宣言しちゃうってのはどうです?
EF や LINQ to SQL は当然のことながら、 DataSet も分散トランザクションに対応してるはずですが。
「MS-DTC の設定が必要でしょ? Oracle が Unix サーバーで動いているせいで、それできないんだわ」
「おかげで、プログラム中でトランザクションが必要になったら Transaction オブジェクト生成してるです」
あ、はい、そうですか。
えーとえーと、EntityFramework と DataSet とで同じ OracleConnection を共有すればいいのかな。
んで、テストメソッドはそのコネクションから生成したトランザクションを管理する、と。
ざっくりとした作り方
EF6 の DbContext には、 DbConnection を受け取るコンストラクタが用意されています。
デザイナで自動生成される Entities にはこのコンストラクタがありませんが、 partial クラス作って自前で拡張しちゃえば OK。
ところが Database First の場合、ここに OracleConnection を与えると実行時に UnintentionalCodeFirstException を吐いてしまいます。
System.Data.Entity.Infrastructure.UnintentionalCodeFirstException: コンテキストは、Database First または Model First のいずれかの開発で EDMX ファイルから生成されたコードを使用して Code First モードで使用中です。これは正常に動作しません。この問題を修正するために、この例外をスローするコード行を削除しないでください。Database First または Model First を使用する場合は、Entity Framework の接続文字列がスタートアップ プロジェクトの app.config または web.config に含まれていることを確認してください。独自の DbConnection を作成している場合は、それが EntityConnection であり、その他の型の DbConnection ではなく、DbConnection を取る基本の DbContext コンストラクターのいずれかに渡すことを確認してください。Code First、Database First および Model First の詳細については、Entity Framework のドキュメントを参照してください: http://go.microsoft.com/fwlink/?LinkId=394715。
エラーメッセージが非常にわかりにくいんですが、OracleConnection の代わりに EntityConnection を与えてやればいいんですね。
EntityConnection というのは EF 用の接続オブジェクトで、生成するにはデータソースへの接続オブジェクト(今回のケースでは OracleConnection) 以外に MetadataWorkspace が必要です。
で、 MetadataWorkspace は EF の接続文字列に設定されている metadata と .edmx が定義されているアセンブリを与えることで生成できます。
こうして生成した EntityConnection を Enitities のコンストラクタに指定しますが、 DbContext のコンストラクタには contextOwnsConnection なる第二引数が存在することに注意が必要です。
この引数に true を指定すると、 Entities が Dispose() された時点で EntityConnection も Dispose() されてしまいます。
DataSet と違って、外部から与えられたコネクションか自分で生成したコネクションか否かを自動判定してくれません。
まぁ、通常は false を指定することになるでしょう。
サンプルコード
ということで、一つの OracleConnection を EF と DataSet とで共有してトランザクション管理するコードは下記のようになります。
// デザイナが自動生成した Entities に対して、 partial クラスを定義する
public partial class SampleEntities
{
// EntityConnection で初期化するコンストラクタ
public SampleEntities( EntityConnection existingConnection, bool contextOwnsConnection )
: base( existingConnection, contextOwnsConnection )
{
}
}
// デザイナが自動生成した TableAdapter に対して、 partial クラスを定義する
public partial class SampleTableAdapter
{
// OracleConnection を外部から設定する
public void SetConnection( OracleConnection conn )
{
this.Connection = conn;
}
// OracleTransaction を外部から設定する
public void SetTransaction( OracleTransaction trans )
{
this.Transaction = trans;
}
}
// OracleConnection を共有するサンプル
public void SameConnTest()
{
var conn_str = ""; // Oracle の接続文字列
var ef_metadata = ""; // EF の接続文字列内の metadata
// MetadataWrokspace の生成
// EntityConnection を生成するには、 OracleConnection の他にこれが必要
var paths = ef_metadata.Split( '|' );
var assemblies = new[] { this.GetType().Assembly };
var workspace = new MetadataWorkspace( paths, assemblies );
// OracleConnection と EntityConnection を生成
using( var oracle_conn = new OracleConnection( conn_str ) )
using( var entity_conn = new EntityConnection( workspace, oracle_conn ) )
{
// Oracle 接続を開く
oracle_conn.Open();
try
{
using( var trans = oracle_conn.BeginTransaction() )
{
// EF の Enitities を生成
using( var entities = new SampleEntities( entity_conn, false ) )
{
// Entities にトランザクションを設定
// コネクションはコンストラクタで設定しているので不要
entities.Database.UseTransaction( trans );
// EF を使って何か処理
}
// DataSet の TableAdapter を生成
using( var adapter = new SampleTableAdapters.SampleTableAdapter() )
using( var data_table = new SampleDataSet.SampleDataTable() )
{
// TableAdapter にコネクションとトランザクションを設定
adapter.Connection = oracle_conn;
adapter.Transaction = trans;
// DataSet を使って何か処理
}
}
}
finally
{
if( oracle_conn.State == System.Data.ConnectionState.Open )
{
// Oracle 接続を閉じる
// OracleConnection.Dispose() で閉じるはずだけど、念のため。
oracle_conn.Close();
}
}
}
}
一応動作確認はしましたが、限りなく概念コードに近いです。
実際の要件に合わせて修正してください。
たとえば先の要件のテストで使用するには、 BeginTransaction() の using 句でテスト対象のアプリケーションのメソッドを呼び出せばいいでしょう。
またこの場合、アプリケーション側は OracleConnection や EntityConnection を外部から指定できるような構成に修正する必要があります。
「え~? そこんとこも組んでくれないの~?」
無茶言わんでください。