EF 和自动映射器。更新嵌套集合



我正在尝试更新国家实体的嵌套集合(城市)。

只是简单的基因和dto:

// EF Models
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<City> Cities { get; set; }
}
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public int CountryId { get; set; }
public int? Population { get; set; }
public virtual Country Country { get; set; }
}
// DTo's
public class CountryData : IDTO
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<CityData> Cities { get; set; }
}
public class CityData : IDTO
{
public int Id { get; set; }
public string Name { get; set; }
public int CountryId { get; set; }
public int? Population { get; set; }
}

代码本身(为了简单起见,在控制台应用程序中进行了测试):

using (var context = new Context())
{
// getting entity from db, reflect it to dto
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
// add new city to dto 
countryDTO.Cities.Add(new CityData 
{ 
CountryId = countryDTO.Id, 
Name = "new city", 
Population = 100000 
});
// change existing city name
countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name";
// retrieving original entity from db
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
// mapping 
AutoMapper.Mapper.Map(countryDTO, country);
// save and expecting ef to recognize changes
context.SaveChanges();
}

此代码引发异常:

操作失败:由于一个或多个外键属性不可为null,因此无法更改关系。当对关系进行更改时,相关的外键属性将设置为null值。如果外键不支持null值,则必须定义新的关系,必须为外键属性分配另一个非null值,或者必须删除不相关的对象。

即使上次映射后的实体看起来很好,并且正确地反映了所有更改。

我花了很多时间寻找解决方案,但没有结果。请帮忙。

问题是您从数据库中检索的country已经有一些城市。当你像这样使用AutoMapper时:

// mapping 
AutoMapper.Mapper.Map(countryDTO, country);

AutoMapper正在做一些事情,比如正确地创建一个IColletion<City>(在您的示例中有一个城市),并将这个全新的集合分配给您的country.Cities属性。

问题是EntityFramework不知道如何处理旧的城市集合。

  • 它应该删除你的旧城市,只承担新的收藏吗
  • 它应该只是合并这两个列表并将两者都保存在数据库中吗

事实上,EF无法为您做出决定。如果你想继续使用AutoMapper,你可以自定义你的映射如下:

// AutoMapper Profile
public class MyProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<CountryData, Country>()
.ForMember(d => d.Cities, opt => opt.Ignore())
.AfterMap(AddOrUpdateCities);
}
private void AddOrUpdateCities(CountryData dto, Country country)
{
foreach (var cityDTO in dto.Cities)
{
if (cityDTO.Id == 0)
{
country.Cities.Add(Mapper.Map<City>(cityDTO));
}
else
{
Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id));
}
}
}
}

Cities使用的Ignore()配置使AutoMapper只保留EntityFramework构建的原始代理引用。

然后,我们只需使用AfterMap()来调用一个操作,该操作正是您所想的:

  • 对于新城市,我们从DTO映射到实体(AutoMapper创建一个新的实例),并将其添加到国家收藏中
  • 对于现有的城市,我们使用Map的重载,其中我们将现有实体作为第二个参数,将城市代理作为第一个参数,因此AutoMapper只更新现有实体的属性

然后你可以保留你的原始代码:

using (var context = new Context())
{
// getting entity from db, reflect it to dto
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
// add new city to dto 
countryDTO.Cities.Add(new CityData 
{ 
CountryId = countryDTO.Id, 
Name = "new city", 
Population = 100000 
});
// change existing city name
countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name";
// retrieving original entity from db
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
// mapping 
AutoMapper.Mapper.Map(countryDTO, country);
// save and expecting ef to recognize changes
context.SaveChanges();
}

这本身并不是OP的答案,但任何今天看到类似问题的人都应该考虑使用AutoMapper。收集它为这些父子集合问题提供了支持,这些问题过去需要处理大量代码。

我很抱歉没有包括一个好的解决方案或更多的细节,但我现在只是加快速度。上面链接上显示的README.md中有一个非常简单的例子。

使用它需要一点重写,但它大大减少了您必须编写的代码量,特别是如果您使用EF并且可以使用AutoMapper.Collection.EntityFramework

当保存更改时,所有城市都被视为已添加,因为EF直到节省时间才考虑这些城市。所以EF试图将null设置为老城区的外键,并插入它而不是更新。

使用ChangeTracker.Entries(),您将了解EF将对CRUD进行哪些更改。

如果你只想手动更新现有城市,你可以简单地做:

foreach (var city in country.cities)
{
context.Cities.Attach(city); 
context.Entry(city).State = EntityState.Modified;
}
context.SaveChanges();

我似乎找到了解决方案:

var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
countryDTO.Cities.Add(new CityData { CountryId = countryDTO.Id, Name = "new city 2", Population = 100000 });
countryDTO.Cities.FirstOrDefault(x => x.Id == 11).Name = "another name";
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
foreach (var cityDTO in countryDTO.Cities)
{
if (cityDTO.Id == 0)
{
country.Cities.Add(cityDTO.ToEntity<City>());
}
else
{
AutoMapper.Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id)); 
}
}
AutoMapper.Mapper.Map(countryDTO, country);
context.SaveChanges();

此代码更新已编辑的项目并添加新项目。但也许有一些陷阱我现在还无法察觉?

非常好的Alisson解决方案。这是我的解决方案。。。正如我们所知,EF不知道请求是更新还是插入,所以我要做的是先用RemoveRange()方法删除,然后发送集合以再次插入。在后台,这就是数据库的工作方式,然后我们可以手动模拟这种行为。

这是代码:

//country object from request for example

var cities = dbcontext.Cities.Where(x=>x.countryId == country.Id);

dbcontext.Cities.RemoveRange(cities);

/* Now make the mappings and send the object this will make bulk insert into the table related */

我花了一些时间为AutoMapper 11+想出了一个更好的解决方案,因为目前没有不使用AfterMap()的EF Core和映射关系集合的解决方案。这并不像它可能的那样高效(需要多个枚举),但它在映射大量子关系时节省了大量模板,并支持源集合和目标集合顺序不相同的条件:

// AutoMapper Profile
public class MyProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<CountryData, Country>()
.ForMember(d => d.Id, opt => opt.MapFrom(x => x.Id))
// relationship collections must be ignored, CountryDataMappingAction will take care of it
.ForMember(d => d.Cities, opt => opt.Ignore())
.AfterMap<CountryDataMappingAction>();
}
public class CountryDataMappingAction : BaseCollectionMapperAction<CountryData, Country>
{
public override void Process(CountryData source, Country destination, ResolutionContext context)
{
MapCollection(source.Cities, destination.Cities, (x, y) => x.Id == y.Id, context);
}
}
}
public class BaseCollectionMapperAction<TSource, TDestination> : IMappingAction<TSource, TDestination>
{
public void MapCollection<TCollectionSource, TCollectionDestination>(IEnumerable<TCollectionSource> sourceCollection, IEnumerable<TCollectionDestination> destCollection, Func<TCollectionSource, TCollectionDestination, bool> predicate, ResolutionContext context)
{
MapCollection(sourceCollection.ToList(), destCollection.ToList(), predicate, context);
}
public void MapCollection<TCollectionSource, TCollectionDestination>(IList<TCollectionSource> sourceList, IList<TCollectionDestination> destList, Func<TCollectionSource, TCollectionDestination, bool> predicate, ResolutionContext context)
{
for (var sourceIndex = 0; sourceIndex < sourceList.Count; sourceIndex++)
{
for (var destIndex = 0; sourceIndex < destList.Count; destIndex++)
{
var result = predicate(sourceList[sourceIndex], destList[destIndex]);
if (result)
{
destList[destIndex] = context.Mapper.Map(sourceList[sourceIndex], destList[destIndex]);
break;
}
}
}
}
public virtual void Process(TSource source, TDestination destination, ResolutionContext context)
{
throw new NotImplementedException("You must provide a mapping implementation!");
}
}

最新更新