翼度科技»论坛 编程开发 .net 查看内容

记录一次EF实体跟踪错误

10

主题

10

帖子

30

积分

新手上路

Rank: 1

积分
30
记录一次EF实体跟踪错误

前言

在我写文章编辑接口的,出现了一个实体跟踪的错误,详情如下
System.InvalidOperationException: The instance of entity type 'Tag' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
System.InvalidOperationException:无法跟踪实体类型“Tag”的实例,因为已跟踪具有相同 {'Id'} 键值的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值。
代码

这是控制器,这个是用于修改文章的一个接口
  1. [HttpPut("{id}")]
  2. public async Task<ApiResponse<Post>> Update(string id, PostUpdateDto dto)
  3. {
  4.     var post = _postService.GetById(id);
  5.     if (post == null) return ApiResponse.NotFound($"博客 {id} 不存在");
  6.    
  7.     post = _mapper.Map(dto, post);
  8.     post.LastUpdateTime = DateTime.Now;
  9.     return new ApiResponse<Post>(await _postService.InsertOrUpdateAsync(post));
  10. }
复制代码
PostUpdateDto类如下:
  1. public class PostUpdateDto
  2.     {
  3.         public string Id { get; set; }
  4.         /// <summary>
  5.         /// 标题
  6.         /// </summary>
  7.         public string Title { get; set; }
  8.         /// <summary>
  9.         /// 梗概
  10.         /// </summary>
  11.         public string Summary { get; set; }
  12.         /// <summary>
  13.         /// 内容(markdown格式)
  14.         /// </summary>
  15.         public string Content { get; set; }
  16.         /// <summary>
  17.         /// 分类ID
  18.         /// </summary>
  19.         public int CategoryId { get; set; }
  20.         /// <summary>
  21.         /// 标签
  22.         /// </summary>
  23.         public List<Tag> Tags { get; set; }
  24.     }
复制代码
关键在于public List Tags { get; set; } 这里,我使用了Map去让PostUpdateDto可以映射为post文章类,然后Tags是前端传过来的标签集合,同时也是post的导航属性,控制器是没毛病的,继续往下看。
然后是InsertOrUpdateAsync方法:
传递一个post实体对象,然后对实体进行增改操作,我按照常规的写法,修改标签,肯定是要将文章之前的标签删除掉,然后重新添加,这是正常的流程。PostTags就是标签表,用于维护标签和文章的关系,删除文章标签实际就是删除PostTags表的内容。
可以看到我删除了existingTags查询出的对象,也就是删除当前文章下的标签。然后通过遍历前端传过来的tags对象新增新的书签,之后就是保存到数据库的操作了。流程是没有问题的,但是每次编辑就报Tags实体跟踪错误
  1. public async Task<Post> InsertOrUpdateAsync(Post post)
  2.         {
  3.             using var transaction = await _myDbContext.Database.BeginTransactionAsync();
  4.             try
  5.             {
  6.                 // 是新文章的话,先保存到数据库
  7.                 if (await _myDbContext.posts.FindAsync(post.Id) == null)
  8.                 {
  9.                     post.ViewCount = 0;
  10.                     await _myDbContext.posts.AddAsync(post);
  11.                     await _myDbContext.SaveChangesAsync();
  12.                 }
  13.                 var tags = post.Tags.ToList();
  14.                 // 移除原有的标签,没写引发异常
  15.                 // post.Tags.Clear();
  16.                 var existingTags = await _myDbContext.PostTags
  17.                     .Where(pt => pt.PostId == post.Id)
  18.                     .ToListAsync();
  19.                 _myDbContext.PostTags.RemoveRange(existingTags);
  20.                 //修改
  21.                 foreach (var tagName in tags)
  22.                 {
  23.                     var postTag = new PostTag
  24.                     {
  25.                         PostId = post.Id,
  26.                         TagId = tagName.Id,
  27.                     };
  28.                     _myDbContext.PostTags.Add(postTag);
  29.                 }
  30.                 // 保存更改到数据库
  31.                 await _myDbContext.SaveChangesAsync();
  32.                 // 检查文章中的外部图片,下载并进行替换
  33.                 post.Content = await MdExternalUrlDownloadAsync(post);
  34.                 // 修改文章时,将markdown中的图片地址替换成相对路径再保存
  35.                 post.Content = MdImageLinkConvert(post, false);
  36.                 // 更新文章
  37.                 _myDbContext.posts.Attach(post);
  38.                 _myDbContext.Entry(post).State = EntityState.Modified;
  39.                 await _myDbContext.SaveChangesAsync();
  40.                 // 提交事务
  41.                 await transaction.CommitAsync();
  42.                 return post;
  43.             }
复制代码
错误在开头就说了,无法跟踪实体类型“Tag”的实例,我琢磨的很久,我明明没有查询Tag,也没有删除Tag,我删除的是PostTags表,为什么会报这个错误。而且我是没有做外键约束的。
解决

找了很久,发现了一个致命的漏点,我传递的post对象是一个导航属性的Tags的,如图:

可以看到Tags里面是有PostTags的,所以当我获取所有标签var tags = post.Tags.ToList();同时删除PostTags中的内容的时候_myDbContext.PostTags.RemoveRange(existingTags)然后通过循环 foreach (var tagName in tags) 添加了新的标签,但是我并没有将这些标签从 post.Tags 集合中移除。因此,当我保存更改到数据库时,EF Core 会认为 post.Tags 集合中的标签实例已经被跟踪,导致了异常。
简单来说就是post中的Tags要被清除才行,也就是这行代码必须要写上去
  1. // 移除原有的标签
  2. post.Tags.Clear();
复制代码
结论

当对具有导航属性的数据进行增删改的时候,先看看实体中是否存在这些数据,记得先Clear再执行操作。在Clear之前可以将需要的数据保存到变量中,因为Clear之后,是无法通过实体.(点)出属性了

来源:https://www.cnblogs.com/ZYPLJ/archive/2023/08/23/17651675.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具