我正在尝试更新国家实体的嵌套集合(城市)。
只是简单的基因和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!");
}
}