EF 和自动映射器。更新嵌套集合
EF & Automapper. Update nested collections
我正在尝试更新国家/地区实体的嵌套集合(城市)。
只是简单的实体和 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();
}
此代码抛出异常:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
尽管上次映射后的实体看起来还不错并且正确反映了所有更改。
我花了很多时间寻找解决方案,但没有结果。请帮忙。
问题是您从数据库中检索的 country
已经有一些城市。当您像这样使用 AutoMapper 时:
// mapping
AutoMapper.Mapper.Map(countryDTO, country);
AutoMapper 正在做一些事情,比如正确创建 IColletion<City>
(在您的示例中有一个城市),并将这个全新的 collection 分配给您的 country.Cities
属性。
问题是 EntityFramework 不知道如何处理旧的 collection 城市。
- 它是否应该删除您的旧城市并仅假设新的 collection?
- 是否应该合并两个列表并将它们都保存在数据库中?
事实上,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 映射到 Entity(AutoMapper 创建一个新的
实例)并将其添加到国家/地区的 collection.
- 对于现有城市,我们使用
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();
}
当保存更改时,所有城市都被视为已添加,因为 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() 方法删除,然后发送集合以再次插入。在后台,这就是数据库的工作方式,然后我们可以手动模拟这种行为。
代码如下:
// 来自请求的国家对象,例如 </p>
<p>var cities = dbcontext.Cities.Where(x=>x.countryId == country.Id);</p>
<p>dbcontext.Cities.RemoveRange(城市);</p>
<p>/* 现在进行映射并发送对象,这将批量插入到 table 相关 */
这本身并不是对 OP 的回答,但今天看到类似问题的任何人都应该考虑使用 AutoMapper.Collection。它为这些 parent-child 收集问题提供了支持,这些问题过去需要大量代码才能处理。
对于没有提供好的解决方案或更多细节,我深表歉意,但我现在只是在加快速度。上面 link 中显示的 README.md 中有一个很好的简单示例。
使用它需要一些重写,但它极大地 减少了您必须编写的代码量,特别是如果您使用的是 EF 并且可以利用AutoMapper.Collection.EntityFramework
.
我花了一些时间为 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!");
}
}
我正在尝试更新国家/地区实体的嵌套集合(城市)。
只是简单的实体和 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();
}
此代码抛出异常:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
尽管上次映射后的实体看起来还不错并且正确反映了所有更改。
我花了很多时间寻找解决方案,但没有结果。请帮忙。
问题是您从数据库中检索的 country
已经有一些城市。当您像这样使用 AutoMapper 时:
// mapping
AutoMapper.Mapper.Map(countryDTO, country);
AutoMapper 正在做一些事情,比如正确创建 IColletion<City>
(在您的示例中有一个城市),并将这个全新的 collection 分配给您的 country.Cities
属性。
问题是 EntityFramework 不知道如何处理旧的 collection 城市。
- 它是否应该删除您的旧城市并仅假设新的 collection?
- 是否应该合并两个列表并将它们都保存在数据库中?
事实上,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 映射到 Entity(AutoMapper 创建一个新的 实例)并将其添加到国家/地区的 collection.
- 对于现有城市,我们使用
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();
}
当保存更改时,所有城市都被视为已添加,因为 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() 方法删除,然后发送集合以再次插入。在后台,这就是数据库的工作方式,然后我们可以手动模拟这种行为。
代码如下:
// 来自请求的国家对象,例如 </p>
<p>var cities = dbcontext.Cities.Where(x=>x.countryId == country.Id);</p>
<p>dbcontext.Cities.RemoveRange(城市);</p>
<p>/* 现在进行映射并发送对象,这将批量插入到 table 相关 */
这本身并不是对 OP 的回答,但今天看到类似问题的任何人都应该考虑使用 AutoMapper.Collection。它为这些 parent-child 收集问题提供了支持,这些问题过去需要大量代码才能处理。
对于没有提供好的解决方案或更多细节,我深表歉意,但我现在只是在加快速度。上面 link 中显示的 README.md 中有一个很好的简单示例。
使用它需要一些重写,但它极大地 减少了您必须编写的代码量,特别是如果您使用的是 EF 并且可以利用AutoMapper.Collection.EntityFramework
.
我花了一些时间为 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!");
}
}