kal*_*ies 9 c# entity-framework entity-framework-core
将附加实体的实体附加到现有实体时,我遇到了一个问题(我将现有实体称为数据库中已存在的实体,并正确设置了PK).
问题是使用Entity Framework Core 1.1.0时.这与Entity Framework 7(Entity Framework Core的初始名称)完美配合.
我没有尝试使用EF6或EF Core 1.0.0.
我想知道这是回归,还是故意改变行为.
该模型
该试验模型在于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; }
}
Run Code Online (Sandbox Code Playgroud)
数据库上下文
所有关系都明确定义(没有冗余).
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);
}
Run Code Online (Sandbox Code Playgroud)
所有具体实体也在此模型中公开:
public DbSet<Person> PersonCollection { get; set; }
public DbSet<Place> PlaceCollection { get; set; }
public DbSet<PersonPlace> PersonPlaceCollection { get; set; }
Run Code Online (Sandbox Code Playgroud)
保理数据访问
我正在使用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();
}
Run Code Online (Sandbox Code Playgroud)
重现异常的步骤
创建一个人并保存.创建一个地方并保存.
// 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();
Run Code Online (Sandbox Code Playgroud)
人员和地点都在数据库中,因此定义了主键.PK由SQL Server生成为标识列.
重新加载人和地点,作为分离的实体(它们被分离的事实用于通过Web API模拟http发布实体的场景,例如在客户端使用angularJS).
// detached entities
var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW");
var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack");
Run Code Online (Sandbox Code Playgroud)
将此人添加到该地点并保存:
castleblackPlace.PersonPlaceCollection.Add(
new PersonPlace() { Person = jonSnow }
);
placeRepo.Update(castleblackPlace);
placeRepo.SaveChanges();
Run Code Online (Sandbox Code Playgroud)
在SaveChanges
抛出一个异常,因为EF核心1.1.0试图INSERT现有的人,而不是做一个的UPDATE(虽然它的主键值设置).
例外细节
Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误.有关详细信息,请参阅内部异常 ---> 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;
});
Run Code Online (Sandbox Code Playgroud)
最后有什么问题???
为什么我们必须手动跟踪实体状态,而以前版本的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
}
Run Code Online (Sandbox Code Playgroud)
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"
}
}
Run Code Online (Sandbox Code Playgroud)
使用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
}
Run Code Online (Sandbox Code Playgroud)
和相应的项目文件:
{
"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"
}
}
Run Code Online (Sandbox Code Playgroud)
经过一些研究,阅读评论,博客文章,最重要的是,EF团队成员对我在GitHub回购中提交的问题的回答,看来我在问题中注意到的行为不是错误,而是一个功能EF Core 1.0.0和1.1.0.
[...] 1.1在每当我们确定一个实体应该被添加因为它没有密钥集时,那么作为该实体的子节点发现的所有实体也将被标记为已添加.
(亚瑟维克斯 - > 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;
});
Run Code Online (Sandbox Code Playgroud)
但是(之前没有任何说法,但实际上很重要!)有些东西我已经失踪太久了,这导致我HeadAcheExceptions:
如果发现已经由上下文跟踪的实体,则不处理该实体(并且不遍历它的导航属性).
(来源:该方法的智能感知!)
因此,在发布断开连接的实体之前,确保上下文没有任何内容可能是安全的:
public virtual void DetachAll()
{
foreach (var entityEntry in _context.ChangeTracker.Entries().ToArray())
{
if (entityEntry.Entity != null)
{
entityEntry.State = EntityState.Detached;
}
}
}
Run Code Online (Sandbox Code Playgroud)
客户端状态映射
另一种方法是处理客户端的状态,发布实体(因此通过设计断开连接),并根据客户端状态设置其状态.
首先,定义一个将客户端状态映射到实体状态的枚举(只缺少分离状态,因为没有意义):
public enum ObjectState
{
Unchanged = 1,
Deleted = 2,
Modified = 3,
Added = 4
}
Run Code Online (Sandbox Code Playgroud)
然后,使用该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;
});
Run Code Online (Sandbox Code Playgroud)
使用这种方法,我使用一个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;
}
Run Code Online (Sandbox Code Playgroud)
乐观/启发式方法
这就是我实际实现的.我有旧方法的混合(意思是如果实体有未定义的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;
});
Run Code Online (Sandbox Code Playgroud)