Я борюсь с тем, что казалось парой основных операций.
Скажем, у меня есть класс с именем Master:
public class Master
{
public Master()
{
Children = new List<Child>();
}
public int Id { get; set; }
public string SomeProperty { get; set; }
[ForeignKey("SuperMasterId")]
public SuperMaster SuperMaster { get; set; }
public int SuperMasterId { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public string SomeDescription { get; set; }
public decimal Count{ get; set; }
[ForeignKey("RelatedEntityId")]
public RelatedEntity RelatedEntity { get; set; }
public int RelatedEntityId { get; set; }
[ForeignKey("MasterId")]
public Master Master { get; set; }
public int MasterId { get; set; }
}
У нас есть такое действие контроллера:
public async Task<OutputDto> Update(UpdateDto updateInput)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update properties
entity.SomeProperty = "Updated value";
entity.SuperMaster.Id = updateInput.SuperMaster.Id;
foreach (var child in input.Children)
{
if (entity.Children.All(x => x.Id != child.Id))
{
// This input child doesn't exist in entity.Children -- add it
// Mapper.Map uses AutoMapper to map from the input DTO to entity
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// The input child exists in entity.Children -- update it
var oldChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (oldChild == null)
{
continue;
}
// The mapper will also update child.RelatedEntity.Id
Mapper.Map(child, oldChild);
}
foreach (var child in entity.Children.Where(x => x.Id != 0).ToList())
{
if (input.Children.All(x => x.Id != child.Id))
{
// The child doesn't exist in input anymore, mark it for deletion
child.Id = -1;
}
}
entity = await _masterRepository.UpdateAsync(entity);
// Use AutoMapper to map from entity to DTO
return MapToEntityDto(entity);
}
Теперь метод репозитория (MasterRepository):
public async Task<Master> UpdateAsync(Master entity)
{
var superMasterId = entity.SuperMaster.Id;
// Make sure SuperMaster properties are updated in case the superMasterId is changed
entity.SuperMaster = await Context.SuperMasters
.FirstOrDefaultAsync(x => x.Id == superMasterId);
// New and updated children, skip deleted
foreach (var child in entity.Children.Where(x => x.Id != -1))
{
await _childRepo.InsertOrUpdateAsync(child);
}
// Handle deleted children
foreach (var child in entity.Children.Where(x => x.Id == -1))
{
await _childRepo.DeleteAsync(child);
entity.Children.Remove(child);
}
return entity;
}
И, наконец, соответствующий код из ChildrenRepository:
public async Task<Child> InsertOrUpdateAsync(Child entity)
{
if (entity.Id == 0)
{
return await InsertAsync(entity, parent);
}
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
// We have already updated child properties in the controller method
// and it's expected that changed entities are marked as changed in EF change tracker
return entity;
}
public async Task<Child> InsertAsync(Child entity)
{
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
entity = Context.Set<Child>().Add(entity).Entity;
// We need the entity Id, hence the call to SaveChanges
await Context.SaveChangesAsync();
return entity;
}
Свойство Context
на самом деле равно DbContext
, и транзакция запускается в фильтре действий. Если действие вызывает исключение, фильтр действий выполняет откат, а если нет, вызывает SaveChanges.
Отправляемый объект ввода выглядит следующим образом:
{
"someProperty": "Some property",
"superMaster": {
"name": "SuperMaster name",
"id": 1
},
"children": [
{
"relatedEntity": {
"name": "RelatedEntity name",
"someOtherProp": 20,
"id": 1
},
"count": 20,
"someDescription": "Something"
}],
"id": 10
}
Таблица Masters
в настоящее время имеет одну запись с идентификатором 10. У нее нет дочерних элементов.
Выбрасывается исключение:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
Что тут происходит? Я думал, что EF должен отслеживать изменения, и это включает в себя знание того, что мы вызвали SaveChanges в этом внутреннем методе.
EDIT Удаление этого вызова SaveChanges ничего не меняет. Кроме того, я не смог найти инструкции SQL INSERT или UPDATE, сгенерированные EF, при просмотре того, что происходит в SQL Server Profiler.
EDIT2 Оператор INSERT присутствует при вызове SaveChanges, но по-прежнему отсутствует оператор UPDATE для главного объекта.
Id
равным -1, EF выдает операторыDELETE
, которые не влияют ни на одну строку. Вы никогда не должны изменять значения первичного ключа. - person Gert Arnold   schedule 21.01.2018