2017-01-02 12 views
0

既存のエンティティに参照プロパティを保持するエンティティ(EF APIのAddメソッド)または更新(Update EF APIメソッド)エンティティを挿入するときに問題が発生しました。エンティティ(私は、既存のエンティティをデータベースに既に存在し、PKが適切に設定されているエンティティと呼んでいます)。既存のエンティティ(エンティティフレームワークコア1.1.0)との関係で新しいエンティティを挿入するときの問題

モデルはPlacePersonAddress、およびStatusで構成されています

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

私は「場所」のための全体のグラフを作成した場合、アドレスを入力して1ステップで保存すれば、すべて問題ありません。

Addresesでプレースを作成して保存しても、それでも問題はありません。 最後に、既存の人物を追加してプレイスを再保存するときに例外があります。EFは実際に既存の人物を挿入しようとしますが、EFが提供されたIDを持つ行を挿入しようとしたためSQL Serverがエラーをスローします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

{ "バージョン" のファイル:「1.0。0- *」、 "buildOptions":{ "emitEntryPoint":真 }、

"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列に明示的な値を挿入できません。

+0

これは多くのコードです。あなたはその例外をいつ取得しますか?例外に関するあなたの質問は何ですか? – CodingYoshi

+0

私はステップ3の例外があります。例外は想定されていませんので、問題は私のコードまたはEFCore 1.1.0(まだEF7という名前のアルファバージョンで動作していたので)に問題があります。 – kall2sollies

+0

答え、コメント、私自身の研究によると、私はより大きな問題をターゲットにしていたので、問題を完全に書き直しました。 – kall2sollies

答えて

1

コードを変更しましたので、ご確認ください。

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

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

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); 

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

第2に、これらの変更のために、クリアプロセスによって注文も変更されるはずです。あなたのStep 1

// 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(); 

私はPersonに1を推測し、Placeで他方が同じでなければなりませんので、私は、CatsleBlackでアドレスを抽出します。

だから、あなたは新しい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です。

+0

その時間をいただきありがとうございます。私のコードであなたのバージョンをセットアップし、実際にうまくいきます。しかし、私の現実世界のプロジェクト(まだ動作しません)には2つの違いがあります。まず、PlaceとPersonの関係は、多くのものか​​ら多くのものです(もちろん、参加エンティティもあります)。第2に、MVC/WebAPI/Angular Archiであるため、getとPOSTの間でコンテキストが同じではありません。だから私は私のリポジトリ間でコンテキストを共有していなかったのです(しかし、あなたは要求スコープ内で、コンテキストを共有しています)。 – kall2sollies

+0

ここでは、PlaceとPersonの関係が多かれ少なかれ多くなっているコードの更新版です:https://gist.github.com/kall2sollies/3734d011f9b724f4131e4d5242c47028。共有されたコンテキストではまだ動作しています。だから、私はバインディングから出てくる問題がHTTPに投稿された後にコンテキストに戻ってくるのだろうか? – kall2sollies

+1

こんにちは@ kall2sollies、私の遅い応答にごめんなさい。存在エンティティをdbで更新する必要がある場合は、エンティティのプロパティ値を更新するよりも、まずdbの主キーで取得する必要があります(エンティティ間の関係があるため、このタスクを実行するために 'AutoMapper 'を使用できます) 。私の推測では、HTTPから投稿したオブジェクトとdbから取得したオブジェクトは同じではありません。ポストの関係の値がnullになる可能性があるからです。 – kcwu

関連する問題