如何更新或删除相关集合?
How to update or delete related collections?
我正在使用 Abp 版本 3.6.2、ASP.Net Core 2.0 和 free startup template 用于多页 Web 应用程序。我有以下数据模型:
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 MyDbContext : AbpZeroDbContext<Tenant, Role, User, MyDbContext> {
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.
除此之外,问题是如何在更新Person
对象时删除不存在的PersonPhones
?以前,直接使用 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?
P.S.: 应用服务
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
时,将返回正确的对象。我假设问题出在缓存或事务中。我做错了什么?
目前我解决了这个问题。
一开始有必要禁用集合映射,因为 AutoMapper 会重写它们,因此 EntityFramework 将这些集合定义为新实体并尝试将它们添加到数据库中。要禁用集合映射,它需要创建一个继承自 AutoMapper.Profile 的 class:
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);
}
}
}
这样的代码可能不是最优的或者违反了任何原则。在这种情况下,我会很高兴知道该怎么做。
我正在使用 Abp 版本 3.6.2、ASP.Net Core 2.0 和 free startup template 用于多页 Web 应用程序。我有以下数据模型:
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 MyDbContext : AbpZeroDbContext<Tenant, Role, User, MyDbContext> {
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.
除此之外,问题是如何在更新Person
对象时删除不存在的PersonPhones
?以前,直接使用 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?
P.S.: 应用服务
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
时,将返回正确的对象。我假设问题出在缓存或事务中。我做错了什么?
目前我解决了这个问题。 一开始有必要禁用集合映射,因为 AutoMapper 会重写它们,因此 EntityFramework 将这些集合定义为新实体并尝试将它们添加到数据库中。要禁用集合映射,它需要创建一个继承自 AutoMapper.Profile 的 class:
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);
}
}
}
这样的代码可能不是最优的或者违反了任何原则。在这种情况下,我会很高兴知道该怎么做。