【问题标题】:How to update master detail in Entity Framework using Automapper如何使用 Automapper 更新实体框架中的主细节
【发布时间】:2020-03-05 02:44:27
【问题描述】:

我正在尝试使用 AutoMapper 模拟更新孩子。这些关系与“On Delete Cascade”是一对多的。

我的步骤:

  1. 加载包含详细信息的主文件
  2. 将 master 映射到 masterDTO
  3. 更新/更改 masterDTO 中的详细信息
  4. 将 masterDTO 映射回 master
  5. 保存。

问题是:master 更新了,添加了新的细节。但更新后的细节不是持久性

这是我的课程:

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>().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).find(1);
             // there is master in db with id = 1

        var masterDTO = mapper.Map<Master, MasterDTO>(master);

        masterDTO.masterInfo = "Changed value";
        foreach (DetailDTO element in masterDTO.details) {
            element.detailInfo = "Changed value";
        }

        var newElement = new DetailDTO {id = 0,  masterId = 1, detailInfo="New Detail"};
        masterDTO.details.Add(newElement);

        master = mapper.Map(masterDTO, master);

        context.SaveChanges();

}

上次模拟我得到这个结果

Before :                                After :
Master                                  Master
+----+-------------------+              +----+-------------------+   
| id | masterInfo        |              | id | masterInfo        |
+----+-------------------+              +----+-------------------+
| 1  | Old Master Info 1 |              | 1  | Changed value     |
| 2  | Old Master Info 2 |              | 2  | Old Master Info 2 |
+----+-------------------+              +----+-------------------+

Detail                                  Detail
+----+----------+-------------------+   +----+----------+-------------------+   
| id | masterId | detailInfo        |   | id | masterId | masterInfo        |
+----+----------+-------------------+   +----+----------+-------------------+
| 1  | 1        | Old Detail Info 1 |   | 1  | 1        | old Detail Info 1 |
| 2  | 1        | Old Detail Info 2 |   | 2  | 1        | Old Detail Info 2 |
|    |          |                   |   | 3  | 1        | New detail        | 
+----+----------+-------------------+   +----+----------+-------------------+

2 行详细信息未更新

感谢您的建议。 - 吉谷

【问题讨论】:

    标签: c# entity-framework automapper relationship


    【解决方案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 尝试将其放入上下文中当上下文已经在跟踪匹配的实体时出现错误。

    更新:要通过相关属性启用更改跟踪,您需要将导航属性标记为 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 DbContexts 将跟踪它们使用代理包装器加载的实体。这允许延迟加载相关实体,但更重要的是允许 EF 检测各个列何时更改以在 UPDATE 语句中使用。要使用这种方法,导航属性需要标记为virtual,数据库上下文应配置为自动检测更改。 (默认启用)并且查询应该使用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 是否跟踪对该父级的另一个引用?孩子们渴望加载吗?有孩子被跟踪吗?在这些情况下我们应该怎么做?

    至少我们应该断言传入的父级不为空,没有关联到 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 或 ViewModel 是一种更受欢迎的方法,然后使用上面的方法 #1 来加载、更新和保存实体。关键是要避免混合方法 #2 中的元素。

    【讨论】:

    • 我刚刚删除了这些行。
    • 错误消失,但更改的详细信息和主文件未保存。
    • mapper.Map(masterDto, master) 调用之前和之后,你能检查主控上的修改状态吗? (context.Entry(master).State 的值)听起来它几乎是分离的,但你的例子并没有指向它。 (即使用AsNoTracking 或禁用代理)AHHH .. 您需要进行收集和反向引用virtual 否则没有更改跟踪。编辑答案
    • 另外,在进行这样的更新时,请确保存在 Master 行。如果您知道此测试至少存在 1 条记录,请使用 First 而不是 FirstOrDefault。如果不存在记录,那么您将要使用Add,如果需要记录,请加载、编辑它和SaveChanges
    • 嗨 Steave,.. 是的,我确定有 1 条记录,id =1
    【解决方案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();
    

    【讨论】:

    • 是的,错误消失了,但是更改的数据没有保存.. ???需要更多建议。谢谢
    • 您好 Farhad,当我调试时,更改的值仍未保存。在行 context.SaveChanges() 执行之前,主值已经更改。
    • @JiguJigu Hi :) 断点到达context.Masters.Attach(master);context.Details.Attach(item); 行?
    • master & details 已经包含了更新的值,但是 context.Entry(item).State 是不变的。
    • @JiguJigu 你的意思是master.details 保存更改后没有更改?
    猜你喜欢
    • 1970-01-01
    • 2011-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多