插入與現有實體關係的新實體時的問題(Entity Framework Core 1.1.0)

c# entity-framework-core

我在插入( Add EF API的方法)或更新(EF API的Update方法)實體時遇到了一個問題,這些實體將引用屬性保存到現有實體(我將現有實體稱為數據庫中已存在的實體,並正確設置其PK )。

該模型包括PlacePersonAddressStatus

  • 一個人有很多地址。
  • 一個地方有幾個人,還有幾個地址。
  • 地方,人員和地址都有狀態。
  • 所有實體都有Id,Name,Created date和Modified date(這些字段都是在抽象BaseEntity定義的)

如果我為“地點”創建一個完整的圖表,使用新的人員和新的地址,並將其保存在一個步驟中,一切都很好。

如果我用Addreses創建一個Place然後保存它,它仍然可以。但最後當我添加一個現有的人並重新保存Place時,我有一個例外:EF實際上試圖插入現有的人,並且SQL Server拋出一個錯誤,因為EF試圖插入一個帶有提供的Id的行(PK被設置為由SQL Server生成)。

這意味著默認情況下,EF Core 1.1.0看起來無法正確遍歷關係並發現應添加哪些enitite,以及應忽略或更新哪個enitite。它嘗試插入已將其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'中為identity列插入顯式值。

一般承認的答案

我修改了一些代碼,請查看它。

在課堂上DbRepository ,增加了一個構造函數,以確保有相同DbContext不同DbRepository

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

在類Person添加了2個屬性,以確保PersonPlace之間的關係。

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合法嗎? 是的,了解原因