既存のエンティティとの関係を持つ新しいエンティティを挿入するときの問題(Entity Framework Core 1.1.0)

c# entity-framework-core

質問

既存のエンティティに参照プロパティを保持するエンティティ(EF APIのメソッドのAdd )または更新(EF APIのUpdateメソッド)エンティティ(既存のエンティティをデータベースに既に存在するエンティティと呼び、そのPKが適切に設定されているときに問題が発生しました。 )。

モデルは、 PlacePersonAddress 、およびStatus構成されPlace

  • 人は多くの住所を持っています。
  • 場所には複数の人物といくつかの住所があります。
  • 場所、人物、住所にはステータスがあります。
  • すべてのエンティティには、ID、名前、作成日、変更日があります(これらのフィールドはすべて抽象BaseEntity定義されていBaseEntity

新しいPersonsと新しいAddressを使って、 "Place"のグラフ全体を作成し、それを1ステップで保存すれば、すべて問題ありません。

私がAddresesでPlaceを作成して保存しても、それでもOKです。しかし、最後に既存の人物を追加してプレイスを再保存するとき、私は例外があります。EFは実際に既存の人物を挿入しようとしますが、EFはID付きの行を挿入しようとしたためエラーが発生しますSQL Serverによって生成される)。

つまり、デフォルトでは、EF Core 1.1.0は、関係を正しくトラバースできず、追加する必要があるエンティティと、無視するか更新する必要があるエンティティを見つけることができないように見えます。すでにPKが正の値に設定されているエンティティを挿入しようとします。

いくつかの研究をした後、EF Core 1.1.0 APIの新しいDbContext.ChangeTracker.Track()メソッドを発見しました。ルートエンティティの関係をトラバースして検出されたすべてのエンティティに対してコールバックメソッドを実行できます。これにより、プライマリキーの値に応じて、適切な状態を設定しました。

このコードがなければ( DbRepository.ApplyStates() )、既存のエンティティとリレーションを参照する限り、挿入は機能しません。

EF7とDNX CLIでは、DbRepository.ApplyStates()がなくてもこのシナリオが機能することに注意してください。

再生するソース

モデル、DbContext、リポジトリ、テストコードがすべてそこにあります。

using System;
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)
        {
            Seed();
        }

        private static void Seed()
        {
            // Repo
            var statusRepo = new DbRepository<Status>();
            var personRepo = new DbRepository<Person>();
            var addressRepo = new DbRepository<Address>();
            var placeRepo = new DbRepository<Place>();

            // Status
            if (!statusRepo.GetAll().Any())
            {
                statusRepo.InsertOrUpdate(new Status() { Name = "Active" });
                statusRepo.InsertOrUpdate(new Status() { Name = "Archive" });
                statusRepo.SaveChanges();
            }
            var statusActive = statusRepo.GetSingle(1);
            var statusArchive = statusRepo.GetSingle(2);

            // Delete the non static data
            foreach(var address in addressRepo.GetAll()) addressRepo.Delete(address);
            addressRepo.SaveChanges();
            foreach (var place in placeRepo.GetAll()) placeRepo.Delete(place);
            placeRepo.SaveChanges();
            foreach (var person in personRepo.GetAll()) personRepo.Delete(person);
            personRepo.SaveChanges();

            Console.WriteLine("Cleared any existing data");

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

            // Step 1 : a person with status and addresses is saved
            var personWithAddresses = new Person()
            {
                Name = "Jon SNOW",
                Status = statusActive,
                AddressCollection = new List<Address>()
                {
                    new Address() { City = "Castleblack", Status = statusActive },
                    new Address() { City = "Winterfel", Status = statusArchive }
                }
            };
            personRepo.InsertOrUpdate(personWithAddresses);
            personRepo.SaveChanges();

            Console.WriteLine("Step 1 ok");
            System.Threading.Thread.Sleep(1000);

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

            // Step 2 : Create a place with addresses
            var placeWithAddress = new Place()
            {
                Name = "Castleblack",
                Status = statusActive
            };
            placeWithAddress.AddressCollection.Add(new Address() { City = "Castleblack", Status = statusActive });
            placeRepo.InsertOrUpdate(placeWithAddress);
            placeRepo.SaveChanges();

            Console.WriteLine("Step 2 ok");
            System.Threading.Thread.Sleep(1000);

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

            // Step 3 : add person to this place
            placeWithAddress.PersonCollection.Add(personWithAddresses);
            placeRepo.InsertOrUpdate(placeWithAddress);
            placeRepo.SaveChanges();

            Console.WriteLine("Step 3 ok");
            System.Threading.Thread.Sleep(1000);
        }
    }

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

        public T GetSingle(int id) => _context.Set<T>().FirstOrDefault(e => e.Id == id);

        public IEnumerable<T> GetAll() => _context.Set<T>().AsEnumerable();

        public void Insert(T entity)
        {
            ApplyStates(entity);
            _context.Add(entity);
        }

        public void Update(T entity)
        {
            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 InsertOrUpdate(T entity)
        {
            if (entity.IsNew) Insert(entity); else Update(entity);
        }

        public void SaveChanges()
        {
            var pendingChanges = _context.ChangeTracker.Entries<T>()
                .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified)
                .Select(e => e.Entity)
                .ToList();
            foreach (var entity in pendingChanges)
            {
                entity.Modified = DateTime.Now;
                if (entity.Created == null) entity.Created = DateTime.Now;
            }
            _context.SaveChanges();
        } 
    }

    #region Models
    public abstract class BaseEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime? Created { get; set; }
        public DateTime? Modified { get; set; }
        [NotMapped]
        public bool IsNew => Id <= 0;
    }

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

    public class Address : BaseEntity
    {
        public string Zip { get; set; }
        public string City { get; set; }
        public int? StatusId { get; set; }
        public Status Status { get; set; }
        public int? PersonId { get; set; }
        public Person Person { get; set; }
        public int? PlaceId { get; set; }
        public Place Place { get; set; }
    }

    public class Place : BaseEntity
    {
        public int? StatusId { get; set; }
        public Status Status { get; set; }
        public List<Person> PersonCollection { get; set; } = new List<Person>();
        public List<Address> AddressCollection { get; set; } = new List<Address>();  
    }

    public class Status : BaseEntity { }
    #endregion

    #region Context
    public class MyContext : DbContext
    {
        public DbSet<Status> StatusCollection { get; set; }
        public DbSet<Person> PersonCollection { get; set; } 
        public DbSet<Address> AddressCollection { get; set; }
        public DbSet<Place> PlaceCollection { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            // Basic event fire of model creation
            base.OnModelCreating(builder);

            // Status
            builder.Entity<Status>().ToTable("Status", "Shared");

            // Person
            builder.Entity<Person>().ToTable("Person", "Shared");
            builder.Entity<Person>()
                .HasMany(p => p.AddressCollection)
                .WithOne(a => a.Person);

            // Address
            builder.Entity<Address>().ToTable("Address", "Shared");
            builder.Entity<Address>()
                .HasOne(p => p.Person)
                .WithMany(a => a.AddressCollection);

            // Place
            builder.Entity<Place>().ToTable("Place", "Shared");
            builder.Entity<Place>()
                .HasMany(p => p.AddressCollection)
                .WithOne(p => p.Place);
        }

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

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"
}

}

例外の詳細

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

受け入れられた回答

私はいくつかのコードを修正しました。

クラスDbRepositoryに別のコンストラクタを追加して、異なるDbRepository同じDbContextが存在することを確認します。

public DbRepository(MyContext myContext)
{
    _context = myContext;
}

PersonクラスではPersonPlace関係を保証するために2つのプロパティを追加しました。

public int? PlaceId { get; set; }
public Place Place { get; set; }

関数Seedでは、上記の変更を加えたコードをいくつか修正しました。

まず、リポジトリを初期化する部分です。

// Repo
var myContext = new MyContext();
var statusRepo = new DbRepository<Status>(myContext);
var personRepo = new DbRepository<Person>(myContext);
var addressRepo = new DbRepository<Address>(myContext);
var placeRepo = new DbRepository<Place>(myContext);

これにより、すべてのリポジトリで同じデータベース接続が使用されます。

第二に、これらの変更のために、明確なプロセスによって注文も変更されるはずです。

// Delete the non static data
foreach (var address in addressRepo.GetAll()) addressRepo.Delete(address);
addressRepo.SaveChanges();
foreach (var person in personRepo.GetAll()) personRepo.Delete(person);
personRepo.SaveChanges();
foreach (var place in placeRepo.GetAll()) placeRepo.Delete(place);
placeRepo.SaveChanges();

あなたのStep 1では、 CatsleBlackでアドレスを抽出します。私はPerson内のアドレスを推測し、 Place内の他のアドレスは同じでなければならないからです。

したがって、新しいPersonを初期化すると、それは

var castleBlack = new Address {City = "Castleblack", Status = statusActive};
var personWithAddresses = new Person()
{
        Name = "Jon SNOW",
        Status = statusActive,
        AddressCollection = new List<Address>()
        {
            castleBlack,
            new Address() { City = "Winterfel", 
                            Status = statusArchive }
        }    
};

Place初期化する

var placeWithAddress = new Place()
{
        Name = "Castleblack",
        Status = statusActive
};
placeWithAddress.AddressCollection.Add(castleBlack);

それらは私がやったことであり、成功裏に保存することができます。 dbのPersonレコードにもPlaceIdます。



Related

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