Я использую Abp версии 3.6.2, ASP.Net Core 2.0 и бесплатный шаблон запуска для многостраничного веб-приложения.У меня есть следующая модель данных:
public class Person : Entity<Guid> {
public Person() {
Phones = new HashSet<PersonPhone>();
}
public virtual ICollection<PersonPhone> Phones { get; set; }
public string Name { get; set; }
}
public class PersonPhone : Entity<Guid> {
public PersonPhone() { }
public Guid PersonId { get; set; }
public virtual Person Person { get; set; }
public string Number { get; set; }
}
// DB Context and Fluent API
public class ProcimMSDbContext : AbpZeroDbContext<Tenant, Role, User, ProcimMSDbContext> {
public virtual DbSet<Person> Persons { get; set; }
public virtual DbSet<PersonPhone> PersonPhones { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PersonPhone>(entity => {
entity.HasOne(d => d.Person)
.WithMany(p => p.Phones)
.HasForeignKey(d => d.PersonId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_PersonPhone_Person");
});
}
}
Сущности Person
и PersonPhone
приведены здесь в качестве примера, поскольку существует много«сгруппированные» отношения в модели данных, которые рассматриваются в одном контексте.В приведенном выше примере взаимосвязи между таблицами позволяют сопоставить несколько телефонов с одним человеком, и связанные объекты присутствуют в DTO.Проблема в том, что при создании объекта Person
я могу отправить телефоны с DTO, и они будут созданы с Person
, как и ожидалось.Но когда я обновляю Person
, я получаю сообщение об ошибке:
Abp.AspNetCore.Mvc.ExceptionHandling.AbpExceptionFilter - The instance of
entity type 'PersonPhone' cannot be tracked because another instance
with the same key value for {'Id'} is already being tracked. When attaching
existing entities, ensure that only one entity instance with a given key
value is attached. Consider using
'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting
key values.
В дополнение к этому возникает вопрос, как удалить несуществующий PersonPhones
при обновлении объекта Person
?Ранее при непосредственном использовании EntityFramework Core я делал это:
var phones = await _context.PersonPhones
.Where(p =>
p.PersonId == person.Id &&
person.Phones
.Where(i => i.Id == p.Id)
.Count() == 0)
.ToListAsync();
_context.PersonPhones.RemoveRange(phones);
_context.Person.Update(person);
await _context.SaveChangesAsync();
Вопрос
Возможно ли реализовать подобное поведение с шаблоном хранилища?Если «Да», то возможно ли для этого использовать UoW?
PS: Служба приложений
public class PersonAppService : AsyncCrudAppService<Person, PersonDto, Guid, GetAllPersonsDto, CreatePersonDto, UpdatePersonDto, EntityDto<Guid>>, IPersonAppService {
private readonly IRepository<Person, Guid> _personRepository;
public PersonAppService(IRepository<Person, Guid> repository) : base(repository) {
_personRepository = repository;
}
public override async Task<PersonDto> Update(UpdatePersonDto input) {
CheckUpdatePermission();
var person = await _personRepository
.GetAllIncluding(
c => c.Addresses,
c => c.Emails,
c => c.Phones
)
.Where(c => c.Id == input.Id)
.FirstOrDefaultAsync();
ObjectMapper.Map(input, person);
return await Get(input);
}
}
Динамические вызовы API:
// All O.K.
abp.services.app.person.create({
"phones": [
{ "number": 1234567890 },
{ "number": 9876543210 }
],
"name": "John Doe"
})
.done(function(response){
console.log(response);
});
// HTTP 500 and exception in log file
abp.services.app.person.update({
"phones": [
{
"id": "87654321-dcba-dcba-dcba-000987654321",
"number": 1234567890
}
],
"id":"12345678-abcd-abcd-abcd-123456789000",
"name": "John Doe"
})
.done(function(response){
console.log(response);
});
Обновить
На данный момент, чтобы добавить новые сущности и обновить существующие, я добавил следующий профиль AutoMapper:
public class PersonMapProfile : Profile {
public PersonMapProfile () {
CreateMap<UpdatePersonDto, Person>();
CreateMap<UpdatePersonDto, Person>()
.ForMember(x => x.Phones, opt => opt.Ignore())
.AfterMap((dto, person) => AddOrUpdatePhones(dto, person));
}
private void AddOrUpdatePhones(UpdatePersonDto dto, Person person) {
foreach (UpdatePersonPhoneDto phoneDto in dto.Phones) {
if (phoneDto.Id == default(Guid)) {
person.Phones.Add(Mapper.Map<PersonPhone>(phoneDto));
}
else {
Mapper.Map(phoneDto, person.Phones.SingleOrDefault(p => p.Id == phoneDto.Id));
}
}
}
}
Но есть проблема с удаленными объектами, то есть с объектами, которыенаходятся в базе данных, но не в DTO.Чтобы удалить их, я в цикле сравниваю объекты и вручную удаляю их из базы данных в службе приложений:
public override async Task<PersonDto> Update(UpdatePersonDto input) {
CheckUpdatePermission();
var person = await _personRepository
.GetAllIncluding(
c => c.Phones
)
.FirstOrDefaultAsync(c => c.Id == input.Id);
ObjectMapper.Map(input, person);
foreach (var phone in person.Phones.ToList()) {
if (input.Phones.All(x => x.Id != phone.Id)) {
await _personAddressRepository.DeleteAsync(phone.Id);
}
}
await CurrentUnitOfWork.SaveChangesAsync();
return await Get(input);
}
Здесь есть еще одна проблема: объект, который возвращается из Get
, содержитвсе объекты (удалены, добавлены, обновлены) одновременно.Я также попытался использовать синхронные варианты методов и открыл отдельную транзакцию с UnitOfWorkManager
, например:
public override async Task<PersonDto> Update(UpdatePersonDto input) {
CheckUpdatePermission();
using (var uow = UnitOfWorkManager.Begin()) {
var person = await _companyRepository
.GetAllIncluding(
c => c.Phones
)
.FirstOrDefaultAsync(c => c.Id == input.Id);
ObjectMapper.Map(input, person);
foreach (var phone in person.Phones.ToList()) {
if (input.Phones.All(x => x.Id != phone.Id)) {
await _personAddressRepository.DeleteAsync(phone.Id);
}
}
uow.Complete();
}
return await Get(input);
}
, но это не помогло.Когда Get
вызывается снова на стороне клиента, возвращается правильный объект.Я предполагаю, что проблема либо в кеше, либо в транзакции.Что я делаю не так?