[英]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
: 该模型由Place
, Person
, Address
和Status
:
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个属性,以确保Person
和Place
之间的关系。
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.