插入与现有实体关系的新实体时的问题(Entity Framework Core 1.1.0)

时间:2017-01-02 00:35:24

标签: c# entity-framework-core

我在插入EF API的Add方法)或更新(EF Update EF API方法)实体时遇到了一个问题,这些实体将参考属性保存到现有实体(我称现有实体已经是一个实体存在于数据库中,并正确设置其PK。)

该模型包含PlacePersonAddressStatus

  • 一个人有很多地址。
  • 一个地方有几个人,也是 几个地址。
  • 地点,人物和地址有状态。
  • 所有实体都有ID,名称,创建日期和修改日期(这些字段都以摘要BaseEntity定义)

如果我为“地方”创建一个完整的图表,使用新的人员和新的地址,并将其保存在一个步骤中,一切都很好。

如果我创建一个带有地址的地方然后保存它,它仍然可以。 但最后当我添加一个现有人并重新放置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”:是的     },

"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:更新条目时发生错误。有关详细信息,请参阅内部异常---&GT; System.Data.SqlClient.SqlException:当IDENTITY_INSERT设置为OFF时,无法在表'Person'中为identity列插入显式值。

1 个答案:

答案 0 :(得分:1)

我修改了一些代码,请查看它。

在课程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