我正在尝试使用AutoMapper模拟更新子项。与"删除级联"的关系是一对多的。
我的步骤:
- 加载包含详细信息的master
- 将master映射到master DTO
- 更新/更改master DTO中的详细信息
- 映射回master DTO到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);
}
}
这里是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实体,该实体的属性与上下文无关,因此当上下文已经在跟踪匹配的实体时,他们将使用Add
、Update
或Attach
+.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();