如何使用自动映射器在Entity Framework中更新主要详细信息

问题描述 投票:0回答:2

我正在尝试使用AutoMapper模拟更新子级。该关系与“在删除级联中”是一对多的关系。

我的步骤:

  1. 向主控加载详细信息
  2. 将母版映射到masterDTO
  3. 更新/更改masterDTO中的详细信息
  4. 将masterDTO映射回master
  5. 保存。

我收到一个错误:

发生异常: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

c# entity-framework automapper relationship
2个回答
2
投票

您不需要使用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();

1
投票

当使用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实体,因此他们将使用AddUpdateAttach + .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中的元素。

© www.soinside.com 2019 - 2024. All rights reserved.