私はEntityFramework 4.1(コードファースト)を使用して小さなサンプルプロジェクトに取り組んでいます。私のクラスは次のようになります。
public class Context : DbContext
{
public IDbSet<Person> People { get; set; }
public IDbSet<EmployeeType> EmployeeTypes { get; set; }
}
public class Person
{
[Key]
public int Key { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
virtual public EmployeeType EmployeeType { get; set; }
}
public class EmployeeType
{
[Key]
public int Key { get; set; }
public string Text { get; set; }
virtual public ICollection<Person> People { get; set; }
}
いくつかのEmployeeTypes( "first"、 "second")をデータベースに保存し、最初のタイプのPersonを保存しました。次に、Personを変更します。 Personをロードし、プロパティを変更してから保存することで、これを実行できることを知っています。しかし、代わりにやりたいことは、うまくいくはずだと私には思えますが、これは次のとおりです。
var c = new Context();
var e = c.EmployeeTypes.Single(x => x.Text.Equals("second"));
var p = new Person {
Key = originalKey, // same key
FirstName = "NewFirst", // new first name
LastName = "NewLast", // new last name
EmployeeType = e }; // new employee type
c.Entry(p).State = EntityState.Modified;
c.SaveChanges();
奇妙なことに、これはFirstNameとLastNameを変更しますが、EmployeeTypeは変更しません。新しいコンテキストを取得してこのPersonをリクエストすると、このコードが実行される前と同じように、EmployeeTypeは「first」に設定されたままになります。
スカラープロパティだけでなく、ナビゲーションプロパティを更新するにはどうすればよいですか?(EmployeeTypeの場合、実際に変更する必要があるのは外部キーだけなので、これは特に不可解です。 Personテーブルであり、そのキーはスカラープロパティです。)
(ちなみに、最初にPersonを取得してから、プロパティを1つずつ変更することでこれを実行できることはわかっていますが、ASP.NET MVCでモデルバインディングを使用しているため、この方法の方が簡単なようです。更新されたpersonオブジェクトはすでにmy POSTメソッドにあります。)
別の方法で試すことができます。
var c = new Context();
var e = c.EmployeeTypes.Single(x => x.Text.Equals("second"));
var p = new Person {
Key = originalKey, // same key
FirstName = "NewFirst", // new first name
LastName = "NewLast"}; // new last name
c.People.Attach(p); // Attach person first so that changes are tracked
c.Entry(p).Reference(e => e.EmployeeType).Load();
p.EmployeeType = e; // Now context should know about the change
c.Entry(p).State = EntityState.Modified;
c.SaveChanges();
他のアプローチは、次のようにPerson
エンティティの外部キーを公開することです。
public class Person
{
[Key]
public int Key { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[ForeignKey("EmployeeType")]
public int EmployeeTypeKey { get; set; }
public virtual EmployeeType EmployeeType { get; set; }
}
これにより、Person
とEmployeeType
の間の関係のタイプが独立アソシエーションから外部キーアソシエーションに変更されます。ナビゲーションプロパティを割り当てる代わりに、外部キープロパティを割り当てます。これにより、現在のコードでリレーションを変更できます。
問題は、独立した関連付け(外部キープロパティを使用しない関連付け)が、状態マネージャー/変更トラッカーで個別のオブジェクトとして処理されることです。したがって、人の変更は既存の関係の状態に影響を与えず、新しい関係も設定しませんでした。 MSDNで質問しました DbContext APIでそれを行う方法ですが、DbContext
をObjectContext
にキャストし、ObjectStateManager
とChangeRelationshipState
を使用した場合にのみ可能です。
EFの方法でそれを行うための12の異なる方法を試した後、私がやろうとしていることを行うための合理的なEFコードファーストの方法はないと結論付けました。そこで、リフレクションを使用しました。 DbContext
から継承するクラス用にこのメソッドを作成しました。
public void UpdateFrom<T>(T updatedItem) where T : KeyedItem
{
var originalItem = Set<T>().Find(updatedItem.Key);
var props = updatedItem.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
var value = prop.GetValue(updatedItem, null);
prop.SetValue(originalItem, value, null);
}
}
私のすべてのオブジェクトは抽象クラスを継承し、共通の主キープロパティを持っているため、渡されたものと同じキーを持つ既存のオブジェクトを検索し、新しいオブジェクトから既存のオブジェクトを更新します。 SaveChanges
は後で呼び出す必要があります。
これはコレクションで機能しますが、もっと良い方法が必要だと思います。
var properties = typeof(TEntity).GetProperties();
foreach (var property in properties)
{
if (property.GetCustomAttributes(typeof(OneToManyAttribute), false).Length > 0)
{
dynamic collection = db.Entry(e).Collection(property.Name).CurrentValue;
foreach (var item in collection)
{
if(item.GetType().IsSubclassOf(typeof(Entity)))
{
if (item.Id == 0)
{
db.Entry(item).State = EntityState.Added;
}
else
{
db.Entry(item).State = EntityState.Modified;
}
}
}
}
}
db.Entry(e).State = EntityState.Modified;
db.SaveChanges();