我尝试更新国家/地区实体的嵌套集合(城市)。
只是简单的恩赐和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();
}
此代码抛出异常:
操作失败:无法更改关系,因为一个或多个外键属性不可为空。当对关系进行更改时,相关的外键属性将设置为空值。如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。
即使最后一次映射后的实体看起来很好并且正确反映了所有更改。
我花了很多时间寻找解决方案却没有结果。请帮忙。
答案 0 :(得分:23)
问题是您从数据库中检索的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((d,e) => AddOrUpdateCities(d, e)
);
}
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));
}
}
}
}
用于Ignore()
的{{1}}配置使AutoMapper保留Cities
构建的原始代理参考。
然后我们只使用EntityFramework
来调用一个完全符合您要求的动作:
AfterMap()
的重载,我们将现有实体作为第二个参数传递,将城市代理作为第一个参数传递,因此automapper只更新现有实体的属性。 / LI>
然后你可以保留原始代码:
Map
答案 1 :(得分:1)
当保存更改时,所有城市都被视为添加,因为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();
答案 2 :(得分:0)
好像我找到了解决方案:
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();
此代码更新已编辑的项目并添加新项目。但也许我现在无法发现一些陷阱?
答案 3 :(得分:0)
非常好的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 */
答案 4 :(得分:0)
这本身并不是OP的答案,但是今天遇到类似问题的任何人都应该考虑使用AutoMapper.Collection。它为以前需要大量代码处理的这些父子收集问题提供了支持。
很抱歉,我没有提供好的解决方案或更多细节,但我现在只是加快步伐。上面链接上显示的README.md中有一个非常简单的示例。
使用此方法需要进行一些重写,但是彻底减少了您必须编写的代码量,尤其是如果您使用的是EF并且可以使用{{1} }。