我正在尝试使用AutoMapper模拟更新子级。该关系与“在删除级联中”是一对多的关系。
我的步骤:
我收到一个错误:
发生异常:CLR / System.InvalidOperationException
Microsoft.EntityFrameworkCore.dll中发生类型'System.InvalidOperationException'的未处理异常:'实体类型'Detail'的实例无法跟踪,因为已经跟踪了另一个具有相同'{'id'}键值的实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用'DbContextOptionsBuilder.EnableSensitiveDataLogging'来查看冲突的键值。'
这是我的课程:
public class Master
{
public int id {get;set;}
public string masterInfo {get;set;}
public virtual ICollection<Detail> details { get;set; } = new Collection<Detail>();
}
public class Detail
{
public int id {get;set;}
public int masterId {get;set;}
public virtual Master master {get;set;}
public string detailInfo {get;set;}
}
public class MasterDTO
{
public int id {get;set;}
public string masterInfo {get;set;}
public virtual ICollection<DetailDTO> details { get; set;} = new Collection<DetailDTO>();
}
public class DetailDTO
{
public int id {get;set;}
public int masterId {get;set;}
public virtual MasterDTO master {get;set;}
public string detailInfo {get;set;}
}
DbContext
设置:
public class MyContext : DbContext
{
public DbSet<Master> Masters {get;set;}
public DbSet<Detail> Details {get;set;}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(
@"Server=localhost;Database=Test_AutoMapper;Trusted_Connection=True");
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Master>()
.HasMany<Detail>(m => m.details)
.WithOne(d => d.master)
.HasForeignKey(d => d.masterId)
.OnDelete(DeleteBehavior.Cascade);
}
}
这是StartUp
程序和Automapper的设置:
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Master, MasterDTO>();
cfg.CreateMap<Detail, DetailDTO>();
cfg.CreateMap<MasterDTO, Master>();
cfg.CreateMap<DetailDTO, Detail>();
});
IMapper mapper = config.CreateMapper();
// updated with steve technique
var context = new MyContext();
var master = context.Masters.Include(m => m.details).Single( x => x.id == 1);
var masterDTO = mapper.Map<Master, MasterDTO>(master);
masterDTO.masterInfo = "master - changed to new value";
foreach (DetailDTO element in masterDTO.details) {
element.detailInfo = "detail - changed to new value";
}
var newElement = new DetailDTO {id = 0, masterId = 1, detailInfo="New Detail"};
masterDTO.details.Add(newElement);
master = mapper.Map(masterDTO, master);
context.SaveChanges();
}
执行时出现错误信息
context.SaveChanges();
感谢您的建议。 -Jigu
您不需要使用context.Masters.Add(master);
并且您应该将映射器配置更改为
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Master, MasterDTO>().ForMember(a => a.details, map => map.MapFrom(src => src.details));
cfg.CreateMap<Detail, DetailDTO>();
cfg.CreateMap<MasterDTO, Master>().ForMember(a => a.details, map => map.MapFrom(src => src.details));
cfg.CreateMap<DetailDTO, Detail>();
});
然后,如果未跟踪实体,则进行破解,将其附加到上下文并更新实体
IMapper mapper = config.CreateMapper();
var context = new MyContext();
var master = context.Masters.Include(m => m.details).FirstOrDefault();
var masterDTO = mapper.Map<Master, MasterDTO>(master);
masterDTO.masterInfo = "master - changed to new value";
foreach (DetailDTO element in masterDTO.details)
{
element.detailInfo = "detail - changed to new value";
}
// try to add new element
var newElement = new DetailDTO { id = 0, masterId = 1, detailInfo = "New Detail" };
masterDTO.details.Add(newElement);
Console.Write(context.Entry(master).State.ToString()); //--> Detached
master = mapper.Map(masterDTO, master);
Console.Write(context.Entry(master).State.ToString()); //--> Detached
if (context.Entry(master).State == EntityState.Detached)
{
context.Masters.Attach(master);
}
context.SaveChanges();
当使用mapper.Map执行更新时,代码是正确的,但是您需要删除以下几行:
context.Masters.Add(master);
context.Entry(master).State = EntityState.Modified;
您的上下文已加载并正在跟踪Master实例,因此您所需要做的就是更新属性(Mapper.Map
正在执行的操作,然后在上下文上调用SaveChanges
,EF将负责其余的工作。
Add
用于将新的实体实例添加到DbContext。仅在将实例附加到DbContext时才需要将状态设置为Modified
。您的情况下该实体已经关联。
通常,当开发人员使用默认的mapper.Map调用时,会出现此问题:
// Loads the entity which the Context will track, but then mapper.Map() returns a new instance in the reference. The context is still tracking the first reference.
var master = context.Masters.Single(x => x.MasterId = masterDTO.MasterId);
master = mapper.Map<Master>(masterDTO);
此方法使用与上下文无关的属性创建一个新的Mapper实体,因此他们将使用Add
,Update
或Attach
+ .State = EntitySate.Modified
尝试将其放入Context中,从而导致上下文已经在跟踪匹配的实体时发生错误。
更新:要通过相关属性启用更改跟踪,您需要将导航属性标记为virtual
以启用代理。
public class Master
{
public int id {get;set;}
public string masterInfo {get;set;}
public virtual ICollection<Detail> details { get;set; } = new Collection<Detail>();
}
public class Detail
{
public int id {get;set;}
public int masterId {get;set;}
public virtual Master master {get;set;}
public string detailInfo {get;set;}
}
更新2:更新方案失败。
看来混淆是基于您可以在EF中更新实体的2种主要方式中的概念混淆的。这是两种方法的快速细分:
方法1:使用跟踪/代理。默认情况下,EF DbContext将跟踪使用代理包装器加载的实体。这允许相关实体被延迟加载,但是更重要的是允许EF检测何时将各个列更改为在UPDATE语句中使用。要使用此方法,需要将导航属性标记为virtual
,并且应该将数据库上下文配置为自动检测更改。 (默认启用),并且查询应not使用AsNoTracking
。使用这种方法是加载数据,进行更新和保存更改的最简单方法。对于您要更新的相关实体,请使用Include
渴望加载它们。
var parent = context.Parents.Include(x => x.Children).Single(x => x.ParentId == parentId);
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
context.SaveChanges();
此方法的优点在于它很简单。无需设置修改状态,附加到上下文或担心重复的条目。这种方法的缺点是在尝试更新大量数据时。 DbContext跟踪的行越多,读取和更新所需的时间就越长。另外,诸如不小心在查询中添加AsNoTracking()
或使虚拟属性脱离导航属性之类的简单操作也会使行为更加混乱。
方法2:无跟踪。有时,使用EF的代码将要与分离的实体一起使用。这可能是因为实体正在来回序列化到客户/消费者,或者处理大量实体,或者仅仅是开发团队的首选(尽管很复杂)设计决策。在这种情况下,DbContext不应跟踪实例,并且这些实例应处于Detached状态。因此,一个简单的示例如下所示:
var parent = context.Parents.AsNoTracking().Include(x => x.Children.AsNoTracking()).Single(x => x.ParentId == parentId);
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
现在在这种情况下,我们不能只叫context.SaveChanges()
。不会有错误,但是不会保存任何内容,因为上下文不会跟踪这些实体或检测到更改。
我们必须将它们显式关联回DbContext并设置其修改状态:
context.Attach(parent); // This will attach the parent, and the children, but in an Unmodified state.
context.Entity(parent).State = EntityState.Modified;
foreach(var child in parent.Children)
{
context.Entity(child).State = EntityState.Modified;
}
context.SaveChanges();
// In some cases we will want to detach the parent and children again here.
使用这种方法,您需要更加谨慎地将实体与DbContext重新关联。当所涉及的实体被反序列化时,或者上下文已经存在很长的时间(可能已经在跟踪实体)时,就会出现麻烦。在这些情况下,Attach()
调用可能会失败,因此为了安全起见,应检查上下文是否已跟踪实体。如果实体已传递到要执行更新的方法中,则还应检查该实体是否未被另一个DbContext跟踪。
例如,使用如下所示的方法:
public void UpdateParentDetails(Parent parent)
{
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
_context.Attach(parent);
_context.Entity(parent).State = EntityState.Modified;
foreach(var child in parent.Children)
{
context.Entity(child).State = EntityState.Modified;
}
_context.SaveChanges();
}
这样的代码容易出现问题和误用。传入的父对象是否已经与具有相同_context或另一个上下文实例的Context相关联? _context是否跟踪对该父对象的另一个引用?孩子们渴望吗?有没有追踪到任何孩子?在这些情况下,我们该怎么办?
至少我们应该断言传入的父级不是null,没有与DbContext关联,并检查我们是否尚未跟踪父级:
public void UpdateParentDetails(Parent parent)
{
if (parent == null)
throw new ArgumentNullException("parent");
if (parent.State != EntityState.Detached)
throw new ArgumentException("Parent was associated to a DbContext");
var existingParent = _context.Parents.Local.Single(x => x.ParentId == parentId);
if (existingParent != null)
{
existingParent.PhoneNumber= "0456-7689";
foreach(var child in existingParent.Children)
{
child.IsAttending = true;
}
}
else
{
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
_context.Attach(parent);
_context.Entity(parent).State = EntityState.Modified;
foreach(var child in parent.Children)
{
context.Entity(child).State = EntityState.Modified;
}
}
_context.SaveChanges();
}
如您所见,尝试并确保有关实体状态以及DbContext是否正在跟踪实例的假设变得相当复杂。这就是为什么我通常不建议开发团队尝试与独立实体合作的原因。代码/意图从相当简单的开始,但几乎总是开始遇到导致更多代码,更多复杂性和更多错误的问题。因此,我建议不要将实体传递到读取它们的DbContext范围之外。使用DTO或ViewModels是更可取的方法,然后使用上述方法#1加载,更新和保存实体。关键是要避免混合方法2中的元素。