简体   繁体   English

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

[英]Issue when inserting new entity with relationships to existing entities (Entity Framework Core 1.1.0 )

I have encountered an issue when inserting ( Add method of EF API) or updating ( Update method of EF API) entities holding reference properties to existing entities (I call existing entity an entity that already exists in the database, and has its PK properly set). 将具有引用属性的实体插入(具有EF API的Add方法)或更新具有引用属性的实体(EF API的Update方法)时,我遇到了一个问题(我称现有实体为数据库中已经存在并已正确设置其PK的实体) )。

The model consists in Place , Person , Address , and Status : 该模型由PlacePersonAddressStatus

  • A person has many addresses. 一个人有很多地址。
  • A place has several persons, and also several addresses. 一个地方有几个人,还有几个地址。
  • Places, Persons and Addresses have statuses. 地点,人员和地址具有状态。
  • All entities have an Id, Name, Created date and Modified date (these fields are all defined in an abstract BaseEntity ) 所有实体都有ID,名称,创建日期和修改日期(这些字段均在抽象BaseEntity定义)

If I create a whole graph for a "Place", with new Persons and new Addresses, and save it in one step, everything is fine. 如果我为“地方”创建一个带有新“人”和新地址的完整图形,并将其保存在一个步骤中,那么一切都很好。

If I create a Place with Addreses then save it, it is still ok. 如果我创建一个带有地址的地点,然后保存它,那还是可以的。 But at last when I add an existing person and resave the Place, I have an exception: EF actually tries to insert the existing person, and SQL Server throws an error because EF tried to insert a row with a provided Id (PK are set to be generated by SQL Server). 但是最后,当我添加一个现有人员并重新保存该场所时,我有一个例外:EF实际上试图插入现有人员,并且SQL Server抛出错误,因为EF尝试插入具有提供的ID的行(PK设置为由SQL Server生成)。

That means that by default, EF Core 1.1.0 looks like being unable to properly traverse relationships and discover which enitites should be added, and which one should be ignored or updated. 这意味着默认情况下,EF Core 1.1.0似乎无法正确遍历关系并发现应添加哪些实体,以及应忽略或更新哪些实体。 It tries to insert an entity which already has its PK set to a positive value. 它尝试插入一个已经将其PK设置为正值的实体。

After doing some research, I discovered the new DbContext.ChangeTracker.Track() method of the EF Core 1.1.0 API, and it allows one to execute a callback method on all the entities discovered by traversing the relationships of the root entity. 经过研究,我发现了EF Core 1.1.0 API的新DbContext.ChangeTracker.Track()方法,该方法允许遍历根实体的关系对发现的所有实体执行回调方法。 Thanks to this, I have set up the appropriate State, according to the value of the primary key. 因此,我根据主键的值设置了适当的状态。

Without this code (in DbRepository.ApplyStates() ), none of my insert would work, as long as they would refer a relation to an existing entity. 没有此代码(在DbRepository.ApplyStates() ),只要它们引用与现有实体的关系,我的插入内容都无法正常工作。

Note that with EF7 and the DNX CLI , this scenario would work, even without the DbRepository.ApplyStates() thing. 请注意,使用EF7和DNX CLI ,即使没有DbRepository.ApplyStates(),该方案也可以工作。

Source to reproduce 复制源

everything is in there: models, DbContext, Repository and test code. 一切都在那里:模型,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 file Project.json文件

{ "version": "1.0.0-*", "buildOptions": { "emitEntryPoint": true }, {“ 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"
}

} }

Exception details 例外详情

Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误。 See the inner exception for details. 有关详细信息,请参见内部异常。 ---> System.Data.SqlClient.SqlException: Cannot insert explicit value for identity column in table 'Person' when IDENTITY_INSERT is set to OFF. ---> System.Data.SqlClient.SqlException:当IDENTITY_INSERT设置为OFF时,无法为表“ Person”中的身份列插入显式值。

I modified some code, please review it. 我修改了一些代码,请仔细阅读。

In class DbRepository , added another constructor, to make sure there is the same DbContext in different DbRepository . 在课堂上DbRepository ,增加了一个构造函数,以确保有相同DbContext不同DbRepository

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

In class Person added 2 properties, to ensure the relation between Person and Place . 在课堂上, Person添加了2个属性,以确保PersonPlace之间的关系。

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

In function Seed , modified some code with above modifications. Seed函数中,使用上述修改修改了一些代码。

Firstly, in the part of initialize repository. 首先,在初始化存储库部分。

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

This will make all repository use same database connection. 这将使所有存储库使用相同的数据库连接。

Secondly, due to those changes, the clear process should change the orders, too. 其次,由于这些更改,清除流程也应更改订单。

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

In your Step 1 , I extract the address with CatsleBlack , because I guess the one in Person and the other one in Place should be the same. 在您的Step 1 ,我提取与地址CatsleBlack ,因为我想在一个Person ,另一个在Place应该是一样的。

So, when you initialize a new Person , it will be 因此,当您初始化一个新的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 }
        }    
};

Initialize the Place 初始化Place

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

Those are what I have done, can save successfully. 这些就是我所做的,可以成功保存。 The Person record in db also has its PlaceId . db中的Person记录也具有其PlaceId

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 在其图中附加具有现有和新实体混合的实体(Entity Framework Core 1.1.0) - Attaching an entity with a mix of existing and new entities in its graph (Entity Framework Core 1.1.0) 实体框架 6:实体和关系 - Entity Framework 6: entities and relationships 插入新实例时未执行实体框架核心延迟加载 - Entity framework core lazy loading not performed when inserting new instances 在实体框架核心中使用一对多关系更新连接的实体 - Updating connected entities with one-to-many relationships in entity framework core Entity Framework Core 不支持具有多对多关系的通用抽象实体 - Entity Framework Core not supporting generic abstract entities with many to many relationships 为什么 Entity Framework Core 2.2 C# 在插入具有现有嵌套实体的新实体时响应错误 - Why does Entity Framework Core 2.2 C# respond with an error while inserting a new entity with an existing nested entity 实体框架核心1.1.0数据类型转换 - Entity Framework Core 1.1.0 Data type conversion 带有Entity Framework 6的ObjectContext在现有相关实体上插入重复项 - ObjectContext with Entity Framework 6 inserting duplicates on existing related entities 实体框架插入新实体 - entity framework inserting new entity 实体框架与现有数据的关系 - Entity Framework Relationships with Existing Data
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM