在将包含引用属性的实体附加到现有实体时,我遇到了一个问题(我将现有实体称为数据库中已存在的实体,并正确设置了PK)。
问题在于使用Entity Framework Core 1.1.0时。这与Entity Framework 7(Entity Framework Core的初始名称)完美配合。
我没有尝试使用EF6或EF Core 1.0.0。
我想知道这是回归,还是故意改变行为。
模型
测试模型包含Place
,Person
,以及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<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
}
public class Place : BaseEntity
{
public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
}
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<PersonPlace>()
.HasAlternateKey(o => new { o.PersonId, o.PlaceId });
builder.Entity<PersonPlace>()
.HasOne(pl => pl.Person)
.WithMany(p => p.PersonPlaceCollection)
.HasForeignKey(p => p.PersonId);
builder.Entity<PersonPlace>()
.HasOne(p => p.Place)
.WithMany(pl => pl.PersonPlaceCollection)
.HasForeignKey(p => p.PlaceId);
}
此模型中还公开了所有具体实体:
public DbSet<Person> PersonCollection { get; set; }
public DbSet<Place> PlaceCollection { get; set; }
public DbSet<PersonPlace> PersonPlaceCollection { get; set; }
保理数据访问
我正在使用Repository样式的基类来计算所有与数据访问相关的代码。
public class DbRepository<T> 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<T>()
.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();
}
重现异常的步骤
创建一个人并保存。 创建一个地方并保存。
// Repo
var context = new MyContext()
var personRepo = new DbRepository<Person>(context);
var placeRepo = new DbRepository<Place>(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)。 / p>
// 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
上会抛出异常,因为EF Core 1.1.0会尝试插入现有的人而非执行更新(尽管其主键值已设定。)
例外详情
Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误。有关详细信息,请参阅内部异常---&GT; 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<Person>(context);
var placeRepo = new DbRepository<Place>(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<Person>(context);
var placeRepo = new DbRepository<Place>(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<T> where T : BaseEntity
{
public readonly MyContext _context;
public DbRepository(MyContext context) { _context = context; }
public virtual T FindByNameAsNoTracking(string name) => _context.Set<T>().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 <= 0;
public override string ToString() => $"Id={Id} | Name={Name} | Type={GetType()}";
}
public class Person : BaseEntity
{
public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place });
}
public class Place : BaseEntity
{
public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
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<Person> PersonCollection { get; set; }
public DbSet<Place> PlaceCollection { get; set; }
public DbSet<PersonPlace> PersonPlaceCollection { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// PersonPlace
builder.Entity<PersonPlace>()
.HasAlternateKey(o => new { o.PersonId, o.PlaceId });
builder.Entity<PersonPlace>()
.HasOne(pl => pl.Person)
.WithMany(p => p.PersonPlaceCollection)
.HasForeignKey(p => p.PersonId);
builder.Entity<PersonPlace>()
.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<Person>(context);
var placeRepo = new DbRepository<Place>(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<Person>(context);
var placeRepo = new DbRepository<Place>(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<T> where T : BaseEntity
{
public readonly MyContext _context;
public DbRepository(MyContext context) { _context = context; }
public virtual T FindByNameAsNoTracking(string name) => _context.Set<T>().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 <= 0;
public override string ToString() => $"Id={Id} | Name={Name} | Type={GetType()}";
}
public class Person : BaseEntity
{
public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place });
}
public class Place : BaseEntity
{
public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
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<Person> PersonCollection { get; set; }
public DbSet<Place> PlaceCollection { get; set; }
public DbSet<PersonPlace> PersonPlaceCollection { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// PersonPlace
builder.Entity<PersonPlace>()
.HasAlternateKey(o => new { o.PersonId, o.PlaceId });
builder.Entity<PersonPlace>()
.HasOne(pl => pl.Person)
.WithMany(p => p.PersonPlaceCollection)
.HasForeignKey(p => p.PersonId);
builder.Entity<PersonPlace>()
.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"
}
}
答案 0 :(得分:7)
经过一些研究,阅读评论,博客文章,最重要的是,EF团队成员对我在GitHub回购中提交的问题的回答,看来我在问题中注意到的行为不是错误,但是EF Core 1.0.0和1.1.0的一个功能。
每当我们确定应该添加实体时,[...] 1.1 因为它没有密钥集,所以发现的所有实体都是 该实体的子女也将被标记为已添加。
(亚瑟维克斯 - &gt; https://github.com/aspnet/EntityFramework/issues/7334)
所以我称之为'变通方法'实际上是一种推荐的做法,正如Ivan Stoev在评论中所说的那样。
根据主要关键状态处理实体状态
DbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback)
方法接受根实体(发布,添加,更新,附加,等等),然后遍历根关系图中所有发现的实体,并执行回调行动。
这可以称为之前 _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<EntityEntryGraphNodes> 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
) (以及基于PK值的IsNew访问器)
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;
});