Как обновить или удалить связанные коллекции? - PullRequest
0 голосов
/ 05 июня 2018

Я использую Abp версии 3.6.2, ASP.Net Core 2.0 и бесплатный шаблон запуска для многостраничного веб-приложения.У меня есть следующая модель данных:

enter image description here

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 вызывается снова на стороне клиента, возвращается правильный объект.Я предполагаю, что проблема либо в кеше, либо в транзакции.Что я делаю не так?

1 Ответ

0 голосов
/ 08 июня 2018

На данный момент я решил эту проблему.Вначале необходимо отключить сопоставление коллекций, поскольку AutoMapper перезаписывает их, в результате чего EntityFramework определяет эти коллекции как новые объекты и пытается добавить их в базу данных.Чтобы отключить сопоставление коллекций, необходимо создать класс, который наследуется от AutoMapper.Profile:

using System;
using System.Linq;
using Abp.Domain.Entities;
using AutoMapper;

namespace ProjectName.Persons.Dto {
    public class PersonMapProfile : Profile {
        public PersonMapProfile() {

            CreateMap<UpdatePersonDto, Person>();
            CreateMap<UpdatePersonDto, Person>()
                .ForMember(x => x.Phones, opt => opt.Ignore())
                    .AfterMap((personDto, person) => 
                         AddUpdateOrDelete(personDto, person));
        }

        private void AddUpdateOrDelete(UpdatePersonDto dto, Person person) {

             person.Phones
            .Where(phone =>
                !dto.Phones
                .Any(phoneDto => phoneDto.Id == phone.Id)
            )
            .ToList()
            .ForEach(deleted =>
                person.Phones.Remove(deleted)
            );

            foreach (var phoneDto in dto.Phones) {
                if (phoneDto.Id == default(Guid)) {
                    person.Phones
                    .Add(Mapper.Map<PersonPhone>(phoneDto));
                }
                else {
                    Mapper.Map(phoneDto, 
                        person.Phones.
                        SingleOrDefault(c => c.Id == phoneDto.Id));
                }
            }
        }
    }
}

В приведенном выше примере мы игнорируем сопоставление коллекций и используем функцию обратного вызова для добавления, обновления или удаления телефонов.Теперь ошибка о невозможности отслеживания сущности больше не возникает.Но если вы сейчас запустите этот код, вы увидите, что возвращенный объект имеет как добавленные объекты, так и удаленные.Это связано с тем, что по умолчанию Abp использует UnitOfWork для методов обслуживания приложений.Поэтому вы должны отключить это поведение по умолчанию и использовать явную транзакцию.

using Abp.Application.Services.Dto;
using Abp.Application.Services;
using Abp.Domain.Repositories;
using Abp.Domain.Uow;
using Microsoft.EntityFrameworkCore;
using ProjectName.Companies.Dto;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;

namespace ProjectName.Persons {
    public class PersonAppService : AsyncCrudAppService<Person, PersonDto, Guid, GetAllPersonsDto, CreatePersonDto, UpdatePersonDto, EntityDto<Guid>>, IPersonAppService {

        private readonly IRepository<Person, Guid> _personRepository;
        private readonly IRepository<PersonPhone, Guid> _personPhoneRepository;

        public PersonAppService(
            IRepository<Person, Guid> repository,
            IRepository<PersonPhone, Guid> personPhoneRepository) : base(repository) {
            _personRepository = repository;
            _personPhoneRepository = personPhoneRepository;
        }

        [UnitOfWork(IsDisabled = true)]
        public override async Task<PersonDto> Update(UpdatePersonDto input) {

            CheckUpdatePermission();

            using (var uow = UnitOfWorkManager.Begin()) {

                var person = await _personRepository
                    .GetAllIncluding(
                        c => c.Phones
                    )
                    .FirstOrDefaultAsync(c => c.Id == input.Id);

                ObjectMapper.Map(input, person);

                uow.Complete();
            }

            return await Get(input);
        }
    }
}

Возможно, что такой код не является оптимальным или нарушает какие-либо принципы.В этом случае мне будет приятно узнать, как это сделать.

...