在其图中附加具有现有和新实体混合的实体(Entity Framework Core 1.1.0)

将附加实体的实体附加到现有实体时,我遇到了一个问题(我将现有实体称为数据库中已存在的实体,并正确设置其PK)。

问题是使用Entity Framework Core 1.1.0时。 这与Entity Framework 7(Entity Framework Core的初始名称)完美配合。

我没有尝试使用EF6或EF Core 1.0.0。

我想知道这是回归,还是故意改变行为。

该模型

测试模型包括PlacePerson和Place与Person之间的多对多关系,通过名为PlacePerson的连接实体。

 public abstract class BaseEntity { public int Id { get; set; } public string Name { get; set; } } public class Person : BaseEntity { public int? StatusId { get; set; } public Status Status { get; set; } public List PersonPlaceCollection { get; set; } = new List(); } public class Place : BaseEntity { public List PersonPlaceCollection { get; set; } = new List(); } public class PersonPlace : BaseEntity { public int? PersonId { get; set; } public Person Person { get; set; } public int? PlaceId { get; set; } public Place Place { get; set; } } 

数据库上下文

所有关系都明确定义(没有冗余)。

  protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // PersonPlace builder.Entity() .HasAlternateKey(o => new { o.PersonId, o.PlaceId }); builder.Entity() .HasOne(pl => pl.Person) .WithMany(p => p.PersonPlaceCollection) .HasForeignKey(p => p.PersonId); builder.Entity() .HasOne(p => p.Place) .WithMany(pl => pl.PersonPlaceCollection) .HasForeignKey(p => p.PlaceId); } 

所有具体实体也在此模型中公开:

 public DbSet PersonCollection { get; set; } public DbSet PlaceCollection { get; set; } public DbSet PersonPlaceCollection { get; set; } 

保理数据访问

我正在使用Repository样式的基类来计算所有与数据访问相关的代码。

 public class DbRepository where T : BaseEntity { protected readonly MyContext _context; protected DbRepository(MyContext context) { _context = context; } // AsNoTracking provides detached entities public virtual T FindByNameAsNoTracking(string name) => _context.Set() .AsNoTracking() .FirstOrDefault(e => e.Name == name); // New entities should be inserted public void Insert(T entity) => _context.Add(entity); // Existing (PK > 0) entities should be updated public void Update(T entity) => _context.Update(entity); // Commiting public void SaveChanges() => _context.SaveChanges(); } 

重现exception的步骤

创建一个人并保存。 创建一个地方并保存。

 // Repo var context = new MyContext() var personRepo = new DbRepository(context); var placeRepo = new DbRepository(context); // Person var jonSnow = new Person() { Name = "Jon SNOW" }; personRepo.Add(jonSnow); personRepo.SaveChanges(); // Place var castleblackPlace = new Place() { Name = "Castleblack" }; placeRepo.Add(castleblackPlace); placeRepo.SaveChanges(); 

人和地都在数据库中,因此定义了主键。 PK由SQL Server生成为标识列。

重新加载人和地点,作为分离的实体(它们被分离的事实用于通过Web API模拟http发布实体的场景,例如在客户端使用angularJS)。

 // detached entities var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW"); var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack"); 

将此人添加到该地点并保存:

 castleblackPlace.PersonPlaceCollection.Add( new PersonPlace() { Person = jonSnow } ); placeRepo.Update(castleblackPlace); placeRepo.SaveChanges(); 

SaveChanges上引发exception,因为EF Core 1.1.0尝试INSERT现有人而不是执行UPDATE (尽管设置了其主键值)。

例外细节

Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误。 有关详细信息,请参阅内部exception —> System.Data.SqlClient.SqlException:当IDENTITY_INSERT设置为OFF时,无法在表’Person’中为identity列插入显式值。

之前的版本

此代码可以与EF Core(名为EF7)和DNX CLI的alpha版本完美配合(但不一定优化)。

解决方法

迭代根实体图并正确设置实体状态:

 _context.ChangeTracker.TrackGraph(entity, node => { var entry = node.Entry; var childEntity = (BaseEntity)entry.Entity; entry.State = childEntity.Id <= 0? EntityState.Added : EntityState.Modified; }); 

最后有什么问题???

为什么我们必须手动跟踪实体状态,而以前版本的EF会完全处理它,即使重新附加分离的实体?

完整的复制源(EFCore 1.1.0 – 不工作)

完整的再现源(包括上面描述的解决方法,但其注释被评论。取消注释它将使这个源工作)。

 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) { // One scope for initial data using (var context = new MyContext()) { // Repo var personRepo = new DbRepository(context); var placeRepo = new DbRepository(context); // Database context.Database.EnsureDeleted(); context.Database.EnsureCreated(); /***********************************************************************/ // Step 1 : Create a person var jonSnow = new Person() { Name = "Jon SNOW" }; personRepo.InsertOrUpdate(jonSnow); personRepo.SaveChanges(); /***********************************************************************/ // Step 2 : Create a place var castleblackPlace = new Place() { Name = "Castleblack" }; placeRepo.InsertOrUpdate(castleblackPlace); placeRepo.SaveChanges(); /***********************************************************************/ } // Another scope to put one people in one place using (var context = new MyContext()) { // Repo var personRepo = new DbRepository(context); var placeRepo = new DbRepository(context); // entities var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW"); var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack"); // Step 3 : add person to this place castleblackPlace.AddPerson(jonSnow); placeRepo.InsertOrUpdate(castleblackPlace); placeRepo.SaveChanges(); } } } public class DbRepository where T : BaseEntity { public readonly MyContext _context; public DbRepository(MyContext context) { _context = context; } public virtual T FindByNameAsNoTracking(string name) => _context.Set().AsNoTracking().FirstOrDefault(e => e.Name == name); public void InsertOrUpdate(T entity) { if (entity.IsNew) Insert(entity); else Update(entity); } public void Insert(T entity) { // uncomment to enable workaround //ApplyStates(entity); _context.Add(entity); } public void Update(T entity) { // uncomment to enable workaround //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 SaveChanges() => _context.SaveChanges(); } #region Models public abstract class BaseEntity { public int Id { get; set; } public string Name { get; set; } [NotMapped] public bool IsNew => Id  $"Id={Id} | Name={Name} | Type={GetType()}"; } public class Person : BaseEntity { public List PersonPlaceCollection { get; set; } = new List(); public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place }); } public class Place : BaseEntity { public List PersonPlaceCollection { get; set; } = new List(); public void AddPerson(Person person) => PersonPlaceCollection.Add(new PersonPlace { Person = person, PersonId = person?.Id, PlaceId = 0}); } public class PersonPlace : BaseEntity { public int? PersonId { get; set; } public Person Person { get; set; } public int? PlaceId { get; set; } public Place Place { get; set; } } #endregion #region Context public class MyContext : DbContext { public DbSet PersonCollection { get; set; } public DbSet PlaceCollection { get; set; } public DbSet PersonPlaceCollection { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // PersonPlace builder.Entity() .HasAlternateKey(o => new { o.PersonId, o.PlaceId }); builder.Entity() .HasOne(pl => pl.Person) .WithMany(p => p.PersonPlaceCollection) .HasForeignKey(p => p.PersonId); builder.Entity() .HasOne(p => p.Place) .WithMany(pl => pl.PersonPlaceCollection) .HasForeignKey(p => p.PlaceId); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EF110CoreTest;Trusted_Connection=True;"); } } #endregion } 

EFCore1.1.0项目的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" } } 

使用EF7 / DNX的工作源

 using System.Collections.Generic; using Microsoft.Data.Entity; using System.Linq; using System.ComponentModel.DataAnnotations.Schema; namespace EF7Test { public class Program { public static void Main(string[] args) { // One scope for initial data using (var context = new MyContext()) { // Repo var personRepo = new DbRepository(context); var placeRepo = new DbRepository(context); // Database context.Database.EnsureDeleted(); context.Database.EnsureCreated(); /***********************************************************************/ // Step 1 : Create a person var jonSnow = new Person() { Name = "Jon SNOW" }; personRepo.InsertOrUpdate(jonSnow); personRepo.SaveChanges(); /***********************************************************************/ // Step 2 : Create a place var castleblackPlace = new Place() { Name = "Castleblack" }; placeRepo.InsertOrUpdate(castleblackPlace); placeRepo.SaveChanges(); /***********************************************************************/ } // Another scope to put one people in one place using (var context = new MyContext()) { // Repo var personRepo = new DbRepository(context); var placeRepo = new DbRepository(context); // entities var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW"); var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack"); // Step 3 : add person to this place castleblackPlace.AddPerson(jonSnow); placeRepo.InsertOrUpdate(castleblackPlace); placeRepo.SaveChanges(); } } } public class DbRepository where T : BaseEntity { public readonly MyContext _context; public DbRepository(MyContext context) { _context = context; } public virtual T FindByNameAsNoTracking(string name) => _context.Set().AsNoTracking().FirstOrDefault(e => e.Name == name); public void InsertOrUpdate(T entity) { if (entity.IsNew) Insert(entity); else Update(entity); } public void Insert(T entity) => _context.Add(entity); public void Update(T entity) => _context.Update(entity); public void SaveChanges() => _context.SaveChanges(); } #region Models public abstract class BaseEntity { public int Id { get; set; } public string Name { get; set; } [NotMapped] public bool IsNew => Id  $"Id={Id} | Name={Name} | Type={GetType()}"; } public class Person : BaseEntity { public List PersonPlaceCollection { get; set; } = new List(); public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place }); } public class Place : BaseEntity { public List PersonPlaceCollection { get; set; } = new List(); public void AddPerson(Person person) => PersonPlaceCollection.Add(new PersonPlace { Person = person, PersonId = person?.Id, PlaceId = 0 }); } public class PersonPlace : BaseEntity { public int? PersonId { get; set; } public Person Person { get; set; } public int? PlaceId { get; set; } public Place Place { get; set; } } #endregion #region Context public class MyContext : DbContext { public DbSet PersonCollection { get; set; } public DbSet PlaceCollection { get; set; } public DbSet PersonPlaceCollection { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // PersonPlace builder.Entity() .HasAlternateKey(o => new { o.PersonId, o.PlaceId }); builder.Entity() .HasOne(pl => pl.Person) .WithMany(p => p.PersonPlaceCollection) .HasForeignKey(p => p.PersonId); builder.Entity() .HasOne(p => p.Place) .WithMany(pl => pl.PersonPlaceCollection) .HasForeignKey(p => p.PlaceId); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EF7Test;Trusted_Connection=True;"); } } #endregion } 

和相应的项目文件:

 { "version": "1.0.0-*", "buildOptions": { "emitEntryPoint": true }, "dependencies": { "EntityFramework.Commands": "7.0.0-rc1-*", "EntityFramework.Core": "7.0.0-rc1-*", "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-*" }, "frameworks": { "dnx451": {} }, "commands": { "ef": "EntityFramework.Commands" } } 

经过一些研究,阅读评论,博客文章,最重要的是,EF团队成员对我在GitHub仓库中提交的问题的回答,看来我在问题中注意到的行为不是错误,而是一个functionEF Core 1.0.0和1.1.0。

[…] 1.1在我们确定实体应该被添加因为它没有密钥集时,那么作为该实体的子节点发现的所有实体也将被标记为已添加。

(亚瑟维克斯 – > https://github.com/aspnet/EntityFramework/issues/7334

所以我称之为“变通方法”实际上是一种推荐的做法,正如Ivan Stoev在评论中所说的那样。

根据主键状态处理实体状态

DbContect.ChangeTracker.TrackGraph(object rootEntity, Action callback)方法获取根实体(发布,添加,更新,附加,等等),然后迭代关系图中的所有已发现实体的根,并执行回调Action。

这可以在_context.Add()_context.Update()方法之前调用。

 _context.ChangeTracker.TrackGraph(rootEntity, node => { node.Entry.State = n.Entry.IsKeySet ? EntityState.Modified : EntityState.Added; }); 

但是 (之前没有任何说法,但’实际上很重要!)有些东西我已经失踪太久了,这导致我HeadAcheExceptions:

如果发现已经被上下文跟踪的实体,则不处理该实体(并且不遍历它的导航属性)。

(来源:该方法的智能感知!)

因此,在发布断开连接的实体之前,确保上下文没有任何内容可能是安全的:

 public virtual void DetachAll() { foreach (var entityEntry in _context.ChangeTracker.Entries().ToArray()) { if (entityEntry.Entity != null) { entityEntry.State = EntityState.Detached; } } } 

客户端状态映射

另一种方法是处理客户端的状态,发布实体(因此通过设计断开连接),并根据客户端状态设置其状态。

首先,定义一个枚举,将客户端状态映射到实体状态(只缺少分离状态,因为没有意义):

 public enum ObjectState { Unchanged = 1, Deleted = 2, Modified = 3, Added = 4 } 

然后,使用DbContect.ChangeTracker.TrackGraph(object rootEntity, Action callback)方法根据客户端状态设置实体状态:

 _context.ChangeTracker.TrackGraph(entity, node => { var entry = node.Entry; // I don't like switch case blocks ! if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted; else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged; else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified; else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added; }); 

使用这种方法,我使用BaseEntity抽象类,它共享我的实体的Id (PK),以及ClientState (类型为ObjectState )(和一个IsNew访问器,基于PK值)

 public abstract class BaseEntity { public int Id {get;set;} [NotMapped] public ObjectState ClientState { get;set; } = ObjectState.Unchanged; [NotMapped] public bool IsNew => Id <= 0; } 

乐观/启发式方法

这就是我实际实现的。 我有旧方法的混合(意思是如果实体有未定义的PK,必须添加,如果根有PK,则必须更新),以及客户端状态方法:

 _context.ChangeTracker.TrackGraph(entity, node => { var entry = node.Entry; // cast to my own BaseEntity var childEntity = (BaseEntity)node.Entry.Entity; // If entity is new, it must be added whatever the client state if (childEntity.IsNew) entry.State = EntityState.Added; // then client state is mapped else if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted; else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged; else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified; else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added; });