![](/img/trans.png)
[英]Issue when inserting new entity with relationships to existing entities (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)
將附加實體的實體附加到現有實體時,我遇到了一個問題(我將現有實體稱為數據庫中已存在的實體,並正確設置了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)。
// 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嘗試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;
});
最后有什么問題???
為什么我們必須手動跟蹤實體狀態,而以前版本的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"
}
}
經過一些研究,閱讀評論,博客文章,最重要的是,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)
方法獲取根實體(發布,添加,更新,附加,等等),然后迭代關系圖中的所有已發現實體的根,並執行回調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<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
)(和一個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;
});
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.