如何使用Automapper更新实体框架中的主细节



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

我的步骤:

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

问题是:更新了主控形状,添加了新的详细信息,。但更新后的细节不持久性

以下是我的课程:

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);
}
}

这里是CCD_ 2程序&自动映射器设置:

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行detals未更新

谢谢你的建议。-Jigu

使用mapper执行代码更新时。地图是正确的,但是你需要删除这些线:

context.Masters.Add(master);
context.Entry(master).State = EntityState.Modified;

您的上下文已经加载并正在跟踪Master实例,所以您所需要做的就是更新属性(Mapper.Map正在执行),然后在上下文上调用SaveChanges,EF将负责其余的工作。

Add用于向DbContext添加一个新的实体实例。只有在将实例附加到DbContext时,才需要将状态设置为Modified。在您的情况下,实体已经关联。

通常,当开发人员使用默认映射程序时,会出现此问题。地图调用:

// 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中更新实体的两种主要方式中的概念。以下是两种方法的快速分解:

方法1:使用跟踪/代理。默认情况下,EF DbContexts将跟踪它们使用代理包装器加载的实体。这允许相关实体延迟加载,但更重要的是,允许EF检测单个列何时更改为在UPDATE语句中使用。要使用这种方法,导航属性需要标记为virtual,DB上下文应该配置为自动检测更改。(默认情况下启用),并且查询应该而不是使用AsNoTracking。使用这种方法是加载数据、进行更新和保存更改的最简单方法。对于要更新的Related实体,请使用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不应该跟踪实例,并且这些实例应该处于已分离状态。因此,一个简单的例子是:

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中的元素混合。

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

如果实体没有被跟踪,则chack将它们附加到上下文并更新实体

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();

最新更新