Присоединение сущности с сочетанием существующих и новых сущностей в ее графике (Entity Framework Core 1.1.0)
я столкнулся с проблемой при присоединении сущностей, содержащих ссылочные свойства к существующим сущностям (я называю существующую сущность сущностью, которая уже существует в базе данных и имеет свой ПК правильно установлен).
проблема заключается в использовании Entity Framework Core 1.1.0. Это то, что отлично работает с Entity Framework 7 (начальное имя ядра Entity Framework).
я не пробовал это ни с 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; }
}
контекст базы данных
все отношения явно определены (без избыточность.)
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; }
Факторинг доступа к данным
я использую базовый класс в стиле репозитория для учета всего кода, связанного с доступом к данным.
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 в виде столбцов идентификаторов.
перезагрузите человека и место, как отдельно стоящее сущности (тот факт, что они отсоединены, используется для макета сценария http-объектов через веб-API, например, с 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();
On SaveChanges
создается исключение, потому что EF Core 1.1.0 пытается вставить существующий человек вместо того, чтобы делать обновление (хотя его значение первичного ключа установлено).
сведения об исключении
Microsoft.EntityFrameworkCore.DbUpdateException: произошла ошибка при обновлении записей. Подробнее см. Внутреннее исключение. ---> Система.Данные.В sqlclient.SqlException: не удается вставить явное значение столбца identity в таблицу "Person", если IDENTITY_INSERT имеет значение ВЫКЛЮЧЕНО.
предыдущие версии
этот код будет работать отлично (хотя и не обязательно оптимизирован) с альфа-версией EF Core (с именем EF7) и DNX CLI.
решение
выполните итерацию по корневому графу сущности и правильно установите состояния сущности:
_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
}
.файл json для EFCore1.1.0 проект
{
"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"
}
}
1 ответов
после некоторых исследований, чтения комментариев, сообщений в блоге и, прежде всего, ответа члена команды EF на вопрос, который я представил в репозитории GitHub, кажется, что поведение, которое я заметил в своем вопросе, не является ошибкой, а особенностью EF Core 1.0.0 и 1.1.0.
[...] в 1.1 всякий раз, когда мы определяем, что объект должен быть добавлен поскольку у него нет набора ключей, все объекты, обнаруженные как дети этой сущности также будут помечены как Добавлен.
(Артур Викерс ->https://github.com/aspnet/EntityFramework/issues/7334)
Итак, то, что я назвал "обходным путем", на самом деле является рекомендуемой практикой, как заявил Иван Стоев в своем комментарии.
работа с состояниями сущностей в соответствии с их состоянием первичного ключа
на 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;
});
но (ничего не сказано До " но " на самом деле имеет значение!) есть что-то, чего мне не хватало слишком долго, и это вызвало у меня головные боли:
если обнаружен объект, который уже отслеживается контекст, этот объект не обрабатывается (и его свойства навигации не являются пройденный.)
(источник: intellisense этого метода !)
поэтому, возможно, было бы безопасно убедиться, что контекст свободен от чего-либо перед публикацией отключенного объекта:
public virtual void DetachAll()
{
foreach (var entityEntry in _context.ChangeTracker.Entries().ToArray())
{
if (entityEntry.Entity != null)
{
entityEntry.State = EntityState.Detached;
}
}
}
отображение состояния на стороне клиента
другой подход-иметь дело с государством на стороне клиента, post entities (поэтому отключен по дизайну), и установить их состояние в соответствии с состоянием на стороне клиента.
во-первых, определите перечисление, которое сопоставляет состояния клиента с состояниями сущности (только отсоединенное состояние отсутствует, потому что не имеет смысла):
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 accessor, основанный на значении PK)
public abstract class BaseEntity
{
public int Id {get;set;}
[NotMapped]
public ObjectState ClientState { get;set; } = ObjectState.Unchanged;
[NotMapped]
public bool IsNew => Id <= 0;
}
оптимистический / эвристический подход
это то, что я фактически реализовал. У меня есть смесь старого подхода (это означает, что если En entity имеет PK undefined, он должен быть добавлен, и если корень имеет 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;
});