31

ICollection <>プロパティの変更(多対多の関係)を検出するにはどうすればよいですか?

public class Company
{
    ...

    public virtual ICollection<Employee> Employees { get; set; }
}

using (DataContext context = new DataContext(Properties.Settings.Default.ConnectionString))
{
    Company company = context.Companies.First();
    company.Employees.Add(context.Employees.First());

    context.SaveChanges();
}

public class DataContext : DbContext
{
    public override int SaveChanges()
    {
        return base.SaveChanges();

        // Company's entity state is "Unchanged" in this.ChangeTracker
    }
}
4

2 に答える 2

90

変更されたすべての多対多の関係を見つける方法は次のとおりです。コードを拡張メソッドとして実装しました。

public static class IaExtensions
{
    public static IEnumerable<Tuple<object, object>> GetAddedRelationships(
        this DbContext context)
    {
        return GetRelationships(context, EntityState.Added, (e, i) => e.CurrentValues[i]);
    }

    public static IEnumerable<Tuple<object, object>> GetDeletedRelationships(
        this DbContext context)
    {
        return GetRelationships(context, EntityState.Deleted, (e, i) => e.OriginalValues[i]);
    }

    private static IEnumerable<Tuple<object, object>> GetRelationships(
        this DbContext context,
        EntityState relationshipState,
        Func<ObjectStateEntry, int, object> getValue)
    {
        context.ChangeTracker.DetectChanges();
        var objectContext = ((IObjectContextAdapter)context).ObjectContext;

        return objectContext
            .ObjectStateManager
            .GetObjectStateEntries(relationshipState)
            .Where(e => e.IsRelationship)
            .Select(
                e => Tuple.Create(
                    objectContext.GetObjectByKey((EntityKey)getValue(e, 0)),
                    objectContext.GetObjectByKey((EntityKey)getValue(e, 1))));
    }
}

いくつかの説明。多対多の関係は、EF では独立した関連付け (IA) として表されます。これは、リレーションシップの外部キーがオブジェクト モデルのどこにも公開されていないためです。データベースでは、FK は結合テーブルにあり、この結合テーブルはオブジェクト モデルから隠されています。

IA は、「関係エントリ」を使用して EF で追跡されます。これらは、エンティティ自体ではなく 2 つのエンティティ間の関係を表す点を除いて、DbContext.Entry から取得する DbEntityEntry オブジェクトに似ています。リレーションシップ エントリは DbContext API で公開されないため、それらにアクセスするには ObjectContext にドロップダウンする必要があります。

Employee を Company.Employees コレクションに追加するなどして、2 つのエンティティ間に新しいリレーションシップが作成されると、新しいリレーションシップ エントリが作成されます。この関係は追加済みの状態です。

同様に、2 つのエンティティ間の関係が削除されると、関係エントリは削除済み状態になります。

つまり、変更された多対多の関係 (または実際には変更された IA) を見つけるには、追加および削除された関係エントリを見つける必要があります。これは、GetAddedRelationships と GetDeletedRelationships が行うことです。

関係エントリを取得したら、それらを理解する必要があります。そのためには、インサイダーの知識を知る必要があります。追加された (または変更されていない) 関係エントリの CurrentValues プロパティには、関係の両端にあるエンティティの EntityKey オブジェクトである 2 つの値が含まれます。同様に、厄介なことに少し異なりますが、Deleted リレーションシップ エントリの OriginalValues プロパティには、削除されたリレーションシップの両端にあるエンティティの EntityKey オブジェクトが含まれています。

(そして、はい、これは恐ろしいことです。私を責めないでください。私の時代よりずっと前からです。)

CurrentValues/OriginalValues の違いは、デリゲートを GetRelationships プライベート メソッドに渡す理由です。

EntityKey オブジェクトを取得したら、GetObjectByKey を使用して実際のエンティティ インスタンスを取得できます。これらをタプルとして返します。

これは、いくつかのエンティティ、コンテキスト、および初期化子です。これをテストするために使用しました。(注: テストは大規模なものではありませんでした。)

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Employee> Employees { get; set; }

    public override string ToString()
    {
        return "Company " + Name;
    }
}

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Company> Companies { get; set; }

    public override string ToString()
    {
        return "Employee " + Name;
    }
}

public class DataContext : DbContext
{
    static DataContext()
    {
        Database.SetInitializer(new DataContextInitializer());
    }

    public DbSet<Company> Companies { get; set; }
    public DbSet<Employee> Employees { get; set; }

    public override int SaveChanges()
    {
        foreach (var relationship in this.GetAddedRelationships())
        {
            Console.WriteLine(
                "Relationship added between {0} and {1}",
                relationship.Item1,
                relationship.Item2);
        }

        foreach (var relationship in this.GetDeletedRelationships())
        {
            Console.WriteLine(
                "Relationship removed between {0} and {1}",
                relationship.Item1,
                relationship.Item2);
        }

        return base.SaveChanges();
    }

}

public class DataContextInitializer : DropCreateDatabaseAlways<DataContext>
{
    protected override void Seed(DataContext context)
    {
        var newMonics = new Company { Name = "NewMonics", Employees = new List<Employee>() };
        var microsoft = new Company { Name = "Microsoft", Employees = new List<Employee>() };

        var jim = new Employee { Name = "Jim" };
        var arthur = new Employee { Name = "Arthur" };
        var rowan = new Employee { Name = "Rowan" };

        newMonics.Employees.Add(jim);
        newMonics.Employees.Add(arthur);
        microsoft.Employees.Add(arthur);
        microsoft.Employees.Add(rowan);

        context.Companies.Add(newMonics);
        context.Companies.Add(microsoft);
    }
}

これを使用する例を次に示します。

using (var context = new DataContext())
{
    var microsoft = context.Companies.Single(c => c.Name == "Microsoft");
    microsoft.Employees.Add(context.Employees.Single(e => e.Name == "Jim"));

    var newMonics = context.Companies.Single(c => c.Name == "NewMonics");
    newMonics.Employees.Remove(context.Employees.Single(e => e.Name == "Arthur"));

    context.SaveChanges();
} 
于 2012-03-23T02:02:15.980 に答える
4

あなたの状況の正確なコードを提供することはできませんが、多対多の関係を解消するために、Employees と Company の間にジョイナー テーブルを配置することで、状況が 10 倍に簡素化されると言えます。

于 2011-11-13T11:25:12.630 に答える