EntityFrameworkでエラー

EntityFramewokをいじっていたところ例外がスローされたのでそのときのメモ。

[sourcecode language="csharp"] System.InvalidOperationException はハンドルされませんでした。 Message=1 つのエンティティ オブジェクトを IEntityChangeTracker の複数のインスタンスで参照することはできません。 Source=System.Data.Entity StackTrace: 場所 System.Data.Objects.ObjectContext.VerifyContextForAddOrAttach(IEntityWrapper wrappedEntity) 場所 System.Data.Objects.ObjectContext.AddSingleObject(EntitySet entitySet, IEntityWrapper wrappedEntity, String argumentName) 場所 System.Data.Objects.ObjectContext.AddObject(String entitySetName, Object entity) 場所 System.Data.Entity.Internal.Linq.InternalSet1.<>c__DisplayClass5.<Add>b__4() 場所 System.Data.Entity.Internal.Linq.InternalSet1.ActOnSet(Action action, EntityState newState, Object entity, String methodName) 場所 System.Data.Entity.Internal.Linq.InternalSet1.Add(Object entity) 場所 System.Data.Entity.DbSet1.Add(TEntity entity) 場所 EntityFrameworkSamples.Program.Main(String args) 場所 C:\Projects\cs_mvc3\trunk\EntityFrameworkSamples\EntityFrameworkSamples\Program.cs:行 73 場所 System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String args) 場所 System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args) 場所 Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() 場所 System.Threading.ThreadHelper.ThreadStart_Context(Object state) 場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx) 場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) 場所 System.Threading.ThreadHelper.ThreadStart() InnerException: [/sourcecode]

行おうとしていたことは、テストを作成しようとしたときに異なるコンテキストAから取得したオブジェクトをコンテキストBで処理しようとしたところ上記のような実行時例外がスローされたというものです。

先日のサンプルを元に再現してみました。

[sourcecode language="csharp"] using (var context = new EnterpriseContext()) { // モデルに変更があった場合はデータベースを再作成する // Database.SetInitializer<EnterpriseContext>(new DropCreateDatabaseIfModelChanges<EnterpriseContext>());

// 常にデータベースを再作成する
Database.SetInitializer&lt;EnterpriseContext&gt;(new DropCreateDatabaseAlways&lt;EnterpriseContext&gt;());

// データベースが存在しない場合は再作成する

// Database.SetInitializer<EnterpriseContext>(new CreateDatabaseIfNotExists<EnterpriseContext>());

jobList.ForEach(j =&gt; context.Jobs.Add(j));
rankList.ForEach(r =&gt; context.Ranks.Add(r));
crewList.ForEach(c =&gt; context.Crews.Add(c));
context.SaveChanges();

// 職務でソート
var crews = context.Crews.OrderBy(crew =&gt; crew.JobId);

// このように指定するとRankIdを持っていないモットが最上位となる
//var crews = context.Crews.OrderBy(crew =&gt; crew.RankId);

foreach (var crew in crews)
{
    Console.WriteLine(&quot;{0}[ID:{1}]は階級が{2}、役職が{3}です。&quot;
        , crew.Name
        , crew.Id
        , (crew.Rank == null? &quot;なし&quot; : crew.Rank.Name)
        , crew.Job.Name);
}

// note:異なるコンテキストからデータを取得する
using(var contextTest = new EnterpriseContext())
{
    var crew = contextTest.Crews.Find(1);
    crew.RankId = null;

    // note:元のコンテキストでデータを登録する
    context.Crews.Add(crew);
    context.SaveChanges();
}

} [/sourcecode]

まだよく理解できていないのですが、EntityFrameworkでは生成したオブジェクトの変更管理を行っており、それはコンテキストを通じて実行しているようです。従って、コンテキストをまたがってデータを処理することはできないということでしょうか。 ここで一つ気になったのが、コンテキストから取得していないけれどもすでにデータベースに存在するオブジェクトでを追加した場合どうなるかというところです。今回はJobオブジェクトのid=4について、Nameを「美容師」から「床屋のマスター」に変更してみました。

[sourcecode language="csharp"] using (var context = new EnterpriseContext()) { // モデルに変更があった場合はデータベースを再作成する // Database.SetInitializer<EnterpriseContext>(new DropCreateDatabaseIfModelChanges<EnterpriseContext>());

// 常にデータベースを再作成する
Database.SetInitializer&lt;EnterpriseContext&gt;(new DropCreateDatabaseAlways&lt;EnterpriseContext&gt;());

// データベースが存在しない場合は再作成する

// Database.SetInitializer<EnterpriseContext>(new CreateDatabaseIfNotExists<EnterpriseContext>());

jobList.ForEach(j =&gt; context.Jobs.Add(j));
rankList.ForEach(r =&gt; context.Ranks.Add(r));
crewList.ForEach(c =&gt; context.Crews.Add(c));
context.SaveChanges();

// 職務でソート
var crews = context.Crews.OrderBy(crew =&gt; crew.JobId);

// このように指定するとRankIdを持っていないモットが最上位となる
//var crews = context.Crews.OrderBy(crew =&gt; crew.RankId);

foreach (var crew in crews)
{
    Console.WriteLine(&quot;{0}[ID:{1}]は階級が{2}、役職が{3}です。&quot;
        , crew.Name
        , crew.Id
        , (crew.Rank == null? &quot;なし&quot; : crew.Rank.Name)
        , crew.Job.Name);
}

// using(var contextTest = new EnterpriseContext()) // { // var crew = contextTest.Crews.Find(1); // crew.RankId = null; // // context.Crews.Add(crew); // context.SaveChanges(); // }

var count = (from j in context.Jobs select j.Id).Count();
Console.WriteLine(&quot;処理前の件数:{0}件&quot;, count);

// 存在するデータをPOCOとして生成してsavechangesを行う
var existsJob = new Job() {Name = &quot;床屋のマスター&quot;, Id = 4};
context.Jobs.Add(existsJob);
context.SaveChanges();

// データを取得する
var biyosi = (from j in context.Jobs where j.Name == &quot;美容師&quot; select j).First();
Console.WriteLine(&quot;Id:{0} / Name:{1}&quot;, biyosi.Id, biyosi.Name);

var master = (from j in context.Jobs where j.Name == &quot;床屋のマスター&quot; select j).First();
Console.WriteLine(&quot;Id:{0} / Name:{1}&quot;, master.Id, master.Name);

Console.WriteLine(&quot;処理後の件数:{0}件&quot;, count);

} [/sourcecode]

処理結果は次の通りとなります。

[sourcecode language="csharp"] ジャンリュック・ピカード[ID:2]は階級が大佐、役職が艦長です。 ウィリアム.T.ライカー[ID:3]は階級が中佐、役職が副長です。 データ[ID:4]は階級が少佐、役職が第二副長です。 モット[ID:1]は階級がなし、役職が美容師です。 処理前の件数:4件 Id:4 / Name:美容師 Id:5 / Name:床屋のマスター 処理後の件数:4件 [/sourcecode]

これから考えると、データの更新はコンテキストオブジェクトからデータを取得して行う必要がありますね。DataSetのDataRowと同様の考え方で問題なさそうですね。