グラフに既存のエンティティと新しいエンティティが混在したエンティティを追加する(Entity Framework Core 1.1.0)

c# entity-framework entity-framework-core

質問

既存のエンティティに参照プロパティを持つエンティティを追加するときに問題が発生しました(既存のエンティティをデータベースに既に存在するエンティティと呼び、PKが適切に設定されています)。

問題は、Entity Framework Core 1.1.0を使用する場合です。これは、Entity Framework 7(Entity Framework Coreの初期名)と完全に連携していたものです。

私はEF6でもEF Core 1.0.0でも試していません。

私はこれが退行か、目的に応じた行動の変化かどうか疑問に思います。

モデル

テストモデルは、に存するPlacePerson 、および多対多の名前の参加主体による場所と人との関係PlacePerson

public abstract class BaseEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Person : BaseEntity
{
    public int? StatusId { get; set; }
    public Status Status { get; set; }
    public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
}

public class Place : BaseEntity
{
    public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
}

public class PersonPlace : BaseEntity
{
    public int? PersonId { get; set; }
    public Person Person { get; set; }
    public int? PlaceId { get; set; }
    public Place Place { get; set; }
}

データベースコンテキスト

すべての関係は明確に定義されています(冗長性はありません)。

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        // PersonPlace
        builder.Entity<PersonPlace>()
            .HasAlternateKey(o => new { o.PersonId, o.PlaceId });
        builder.Entity<PersonPlace>()
            .HasOne(pl => pl.Person)
            .WithMany(p => p.PersonPlaceCollection)
            .HasForeignKey(p => p.PersonId);
        builder.Entity<PersonPlace>()
            .HasOne(p => p.Place)
            .WithMany(pl => pl.PersonPlaceCollection)
            .HasForeignKey(p => p.PlaceId);
    }

すべての具体的なエンティティもこのモデルで公開されています。

public DbSet<Person> PersonCollection { get; set; } 
public DbSet<Place> PlaceCollection { get; set; }
public DbSet<PersonPlace> PersonPlaceCollection { get; set; }

データアクセスのファクタリング

私は、リポジトリスタイルの基本クラスを使用して、すべてのデータアクセス関連のコードを考慮しています。

public class DbRepository<T> where T : BaseEntity
{
    protected readonly MyContext _context;
    protected DbRepository(MyContext context) { _context = context; }

    // AsNoTracking provides detached entities
    public virtual T FindByNameAsNoTracking(string name) => 
        _context.Set<T>()
            .AsNoTracking()
            .FirstOrDefault(e => e.Name == name);

    // New entities should be inserted
    public void Insert(T entity) => _context.Add(entity);
    // Existing (PK > 0) entities should be updated
    public void Update(T entity) => _context.Update(entity);
    // Commiting
    public void SaveChanges() => _context.SaveChanges();
}

例外を再現する手順

1人を作成して保存します。 1つの場所を作成して保存します。

// Repo
var context = new MyContext()
var personRepo = new DbRepository<Person>(context);
var placeRepo = new DbRepository<Place>(context);

// Person
var jonSnow = new Person() { Name = "Jon SNOW" };
personRepo.Add(jonSnow);
personRepo.SaveChanges();

// Place
var castleblackPlace = new Place() { Name = "Castleblack" };
placeRepo.Add(castleblackPlace);
placeRepo.SaveChanges();

人物と場所の両方がデータベースにあり、したがって主キーが定義されています。 PKは、SQL ServerによってID列として生成されます。

人と場所をデタッチしたエンティティとしてリロードします(デタッチされているという事実は、Web APIを介してhttp投稿されたエンティティのシナリオを模擬するために使用されます。例えば、クライアント側のangularJSを使用します)。

// detached entities
var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW");
var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack");

その人を場所に追加して保存します:

castleblackPlace.PersonPlaceCollection.Add(
    new PersonPlace()  { Person = jonSnow }
);
placeRepo.Update(castleblackPlace);
placeRepo.SaveChanges();

SaveChangesでは、EF Core 1.1.0がUPDATEを実行するのではなく既存の人物をINSERTしようとするため、例外がスローされます(主キーの値は設定されています)。

例外の詳細

Microsoft.EntityFrameworkCore.DbUpdateException:エントリの更新中にエラーが発生しました。詳細については、内部例外を参照してください。 ---> System.Data.SqlClient.SqlException:IDENTITY_INSERTがOFFに設定されている場合、テーブル 'Person'のID列に明示的な値を挿入できません。

以前のバージョン

このコードはEFコアのアルファ版(EF7)とDNX CLIと完全に(必ずしも最適化されているわけではありませんが)動作します。

回避策

ルートエンティティグラフを繰り返し、エンティティの状態を適切に設定します。

_context.ChangeTracker.TrackGraph(entity, node =>
    {
        var entry = node.Entry;
        var childEntity = (BaseEntity)entry.Entity;
        entry.State = childEntity.Id <= 0? EntityState.Added : EntityState.Modified;
    });

何が最後に質問ですか?

エンティティの状態を手動で追跡する必要があるのはなぜですか。以前のバージョンのEFは、分離されたエンティティを再接続する場合でも、完全に対処しますか?

フル再生ソース(EFCore 1.1.0 - 動作しません)

フル再生ソース(上記の回避策は含まれていますが、その呼び出しはコメントされています。コメントを外すとこのソースが機能します)。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace EF110CoreTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // One scope for initial data
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // Database
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();


                /***********************************************************************/

                // Step 1 : Create a person
                var jonSnow = new Person() { Name = "Jon SNOW" };
                personRepo.InsertOrUpdate(jonSnow);
                personRepo.SaveChanges();

                /***********************************************************************/

                // Step 2 : Create a place
                var castleblackPlace = new Place() { Name = "Castleblack" };
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();

                /***********************************************************************/
            }

            // Another scope to put one people in one place
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // entities
                var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW");
                var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack");

                // Step 3 : add person to this place
                castleblackPlace.AddPerson(jonSnow);
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();
            }

        }
    }

    public class DbRepository<T> where T : BaseEntity
    {
        public readonly MyContext _context;
        public DbRepository(MyContext context) { _context = context; }

        public virtual T FindByNameAsNoTracking(string name) => _context.Set<T>().AsNoTracking().FirstOrDefault(e => e.Name == name);

        public void InsertOrUpdate(T entity)
        {
            if (entity.IsNew) Insert(entity); else Update(entity);
        }

        public void Insert(T entity)
        {
            // uncomment to enable workaround
            //ApplyStates(entity);
            _context.Add(entity);
        }

        public void Update(T entity)
        {
            // uncomment to enable workaround
            //ApplyStates(entity);
            _context.Update(entity);
        }

        public void Delete(T entity)
        {
            _context.Remove(entity);
        }

        private void ApplyStates(T entity)
        {
            _context.ChangeTracker.TrackGraph(entity, node =>
            {
                var entry = node.Entry;
                var childEntity = (BaseEntity)entry.Entity;
                entry.State = childEntity.IsNew ? EntityState.Added : EntityState.Modified;
            });
        }

        public void SaveChanges() => _context.SaveChanges();
    }

    #region Models
    public abstract class BaseEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [NotMapped]
        public bool IsNew => Id <= 0;
        public override string ToString() => $"Id={Id} | Name={Name} | Type={GetType()}";
    }

    public class Person : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place });
    }

    public class Place : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPerson(Person person) => PersonPlaceCollection.Add(new PersonPlace { Person = person, PersonId = person?.Id, PlaceId = 0});
    }

    public class PersonPlace : BaseEntity
    {
        public int? PersonId { get; set; }
        public Person Person { get; set; }
        public int? PlaceId { get; set; }
        public Place Place { get; set; }
    }

    #endregion

    #region Context
    public class MyContext : DbContext
    {
        public DbSet<Person> PersonCollection { get; set; } 
        public DbSet<Place> PlaceCollection { get; set; }
        public DbSet<PersonPlace> PersonPlaceCollection { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);


            // PersonPlace
            builder.Entity<PersonPlace>()
                .HasAlternateKey(o => new { o.PersonId, o.PlaceId });
            builder.Entity<PersonPlace>()
                .HasOne(pl => pl.Person)
                .WithMany(p => p.PersonPlaceCollection)
                .HasForeignKey(p => p.PersonId);
            builder.Entity<PersonPlace>()
                .HasOne(p => p.Place)
                .WithMany(pl => pl.PersonPlaceCollection)
                .HasForeignKey(p => p.PlaceId);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EF110CoreTest;Trusted_Connection=True;");
        }
    }
    #endregion
}

EFCore1.1.0プロジェクトのProject.jsonファイル

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
},

  "dependencies": {
    "Microsoft.EntityFrameworkCore": "1.1.0",
    "Microsoft.EntityFrameworkCore.SqlServer": "1.1.0",
    "Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final" 
},

  "frameworks": {
    "net461": {}
},

  "tools": {
    "Microsoft.EntityFrameworkCore.Tools.DotNet": "1.0.0-preview3-final"
  }
}

EF7 / DNXの作業ソース

using System.Collections.Generic;
using Microsoft.Data.Entity;
using System.Linq;
using System.ComponentModel.DataAnnotations.Schema;

namespace EF7Test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // One scope for initial data
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // Database
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();


                /***********************************************************************/

                // Step 1 : Create a person
                var jonSnow = new Person() { Name = "Jon SNOW" };
                personRepo.InsertOrUpdate(jonSnow);
                personRepo.SaveChanges();

                /***********************************************************************/

                // Step 2 : Create a place
                var castleblackPlace = new Place() { Name = "Castleblack" };
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();

                /***********************************************************************/
            }

            // Another scope to put one people in one place
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // entities
                var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW");
                var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack");

                // Step 3 : add person to this place
                castleblackPlace.AddPerson(jonSnow);
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();
            }

        }
    }

    public class DbRepository<T> where T : BaseEntity
    {
        public readonly MyContext _context;
        public DbRepository(MyContext context) { _context = context; }

        public virtual T FindByNameAsNoTracking(string name) => _context.Set<T>().AsNoTracking().FirstOrDefault(e => e.Name == name);

        public void InsertOrUpdate(T entity)
        {
            if (entity.IsNew) Insert(entity); else Update(entity);
        }

        public void Insert(T entity) => _context.Add(entity);
        public void Update(T entity) => _context.Update(entity);
        public void SaveChanges() => _context.SaveChanges();
    }

    #region Models
    public abstract class BaseEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [NotMapped]
        public bool IsNew => Id <= 0;
        public override string ToString() => $"Id={Id} | Name={Name} | Type={GetType()}";
    }

    public class Person : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place });
    }

    public class Place : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPerson(Person person) => PersonPlaceCollection.Add(new PersonPlace { Person = person, PersonId = person?.Id, PlaceId = 0 });
    }

    public class PersonPlace : BaseEntity
    {
        public int? PersonId { get; set; }
        public Person Person { get; set; }
        public int? PlaceId { get; set; }
        public Place Place { get; set; }
    }

    #endregion

    #region Context
    public class MyContext : DbContext
    {
        public DbSet<Person> PersonCollection { get; set; }
        public DbSet<Place> PlaceCollection { get; set; }
        public DbSet<PersonPlace> PersonPlaceCollection { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            // PersonPlace
            builder.Entity<PersonPlace>()
                .HasAlternateKey(o => new { o.PersonId, o.PlaceId });
            builder.Entity<PersonPlace>()
                .HasOne(pl => pl.Person)
                .WithMany(p => p.PersonPlaceCollection)
                .HasForeignKey(p => p.PersonId);
            builder.Entity<PersonPlace>()
                .HasOne(p => p.Place)
                .WithMany(pl => pl.PersonPlaceCollection)
                .HasForeignKey(p => p.PlaceId);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EF7Test;Trusted_Connection=True;");
        }
    }
    #endregion
}

そして対応するプロジェクトファイル:

{
"version": "1.0.0-*",
"buildOptions": {
    "emitEntryPoint": true
},

"dependencies": {
    "EntityFramework.Commands": "7.0.0-rc1-*",
    "EntityFramework.Core": "7.0.0-rc1-*",
    "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-*"
},

"frameworks": {
    "dnx451": {}
},

"commands": {
    "ef": "EntityFramework.Commands"
}
}

受け入れられた回答

いくつかの調査、コメント、ブログ記事、とりわけEFチームメンバーがGitHubリポジトリに投稿した問題の回答を読んだ後、私の質問で気づいた行動はバグではなく、 EFコア1.0.0および1.1.0の

[...] 1.1では、キーセットがないためにエンティティを追加する必要があると判断した場合、そのエンティティの子として検出されたすべてのエンティティにもAddedとしてマークされます。

(Arthur Vickers - > https://github.com/aspnet/EntityFramework/issues/7334

Ivan Stoev氏のコメントで述べたように、私が「回避策」と呼んでいたのは、実際には推奨される方法です。

主キー状態に従ってエンティティ状態を扱う

DbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback)メソッドは、ルートエンティティ(投稿、追加、更新、アタッチなどDbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback)し、リレーションシップグラフのすべての検出されたエンティティを反復処理しますを実行し、コールバックアクションを実行します。

これは、 _context.Add()または_context.Update()メソッドのに呼び出すことができます。

_context.ChangeTracker.TrackGraph(rootEntity, node => 
{ 
    node.Entry.State = n.Entry.IsKeySet ? 
        EntityState.Modified : 
        EntityState.Added; 
});

しかし、 (何も言わずに 'しかし'実際に問題です!)私はあまりにも長く欠けていたものがあり、それが原因で私はHeadAcheExceptions:

すでにコンテキストによってトラッキングされているエンティティが検出された場合、そのエンティティは処理されません(ナビゲーションプロパティはトラバースされません)。

(出典:その方法のインテリセンス!)

したがって、切断されたエンティティを送信する前に、コンテキストに何も自由でないことを確認することは安全かもしれません。

public virtual void DetachAll()
{
    foreach (var entityEntry in _context.ChangeTracker.Entries().ToArray())
    {
        if (entityEntry.Entity != null)
        {
            entityEntry.State = EntityState.Detached;
        }
    }
}

クライアント側の状態マッピング

別のアプローチは、クライアント側の状態、ポストエンティティ(したがって設計によって切断された状態)を処理し、クライアント側の状態に従って状態を設定することです。

まず、クライアントの状態をエンティティの状態にマップする列挙型を定義します(デタッチ状態が不足しているため意味がありません)。

public enum ObjectState
{
    Unchanged = 1,
    Deleted = 2,
    Modified = 3,
    Added = 4
}

次に、 DbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback)メソッドを使用して、クライアントの状態に応じてエンティティの状態を設定します。

_context.ChangeTracker.TrackGraph(entity, node =>
{
    var entry = node.Entry;
    // I don't like switch case blocks !
    if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted;
    else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged;
    else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified;
    else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added;
});

このアプローチでは、私が使用BaseEntity共有抽象クラス、 Id私のエンティティの(PK)、また、 ClientState (タイプのObjectState (PK値に基づいており、IsNewアクセサを、))

public abstract class BaseEntity
{
    public int Id {get;set;}
    [NotMapped]
    public ObjectState ClientState { get;set; } = ObjectState.Unchanged;
    [NotMapped]
    public bool IsNew => Id <= 0;
}

楽観的/発見的アプローチ

これは私が実際に実装したものです。私は古いアプローチ(enエンティティに未定義のPKがある場合は追加する必要があり、ルートにPKがある場合は更新する必要があります)とクライアント状態のアプローチを組み合わせています:

_context.ChangeTracker.TrackGraph(entity, node =>
{
    var entry = node.Entry;
    // cast to my own BaseEntity
    var childEntity = (BaseEntity)node.Entry.Entity;
    // If entity is new, it must be added whatever the client state
    if (childEntity.IsNew) entry.State = EntityState.Added;
    // then client state is mapped
    else if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted;
    else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged;
    else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified;
    else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added;
});


Related

ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ
ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ