EF Core第二次调用SaveChanges()时添加子实体到父集合失败问题
问题重现
在EF Core中配置父/子实体关系后,业务需要先添加父实体并调用SaveChanges(),后续再为该父实体添加子实体并再次执行SaveChanges()(属于复杂事务流程,必须拆分两次保存操作)。
但出现以下异常:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.
该操作在旧版Entity Framework中可正常执行,且手动将子实体添加到对应DbSet时也能正常运行。
复现代码
internal class Program { static void Main(string[] args) { // 清空之前的数据 { using var db = new BloggingContext(); var posts = db.Posts.ToList(); var blogs = db.Blogs.ToList(); foreach (var post in posts) db.Remove(post); foreach (var blog in blogs) db.Remove(blog); db.SaveChanges(); } try { using var db = new BloggingContext(); var blog = new Blog { Id = 1, Name = "Blog 1", SiteUri = "http://blogs.msdn.com/adonet", Posts = new List<Post>() }; db.Add(blog); var post1 = new Post { Id = 11, BlogId = 1, Title = "Hello World", Content = "I wrote an app using EF Core!" }; blog.Posts.Add(post1); db.SaveChanges(); var post2 = new Post { Id = 12, BlogId = 1, Title = "Hello World 2", Content = "I wrote an app using EF Core 2!" }; blog.Posts.Add(post2); // db.Posts.Add(post2); --> 启用此行可正常运行 db.SaveChanges(); Console.WriteLine("Done"); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } } public class Blog { public int Id { get; set; } public string Name { get; set; } public string SiteUri { get; set; } public virtual ICollection<Post> Posts { get; set; } } public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public DateTime PublishedOn { get; set; } public bool Archived { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } public class BloggingContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } public BloggingContext() { } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer($"Server=.;Database=DbTests;Trusted_Connection=True;Encrypt=False"); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasMany(e => e.Posts) .WithOne(e => e.Blog) .HasForeignKey(e => e.BlogId) .HasPrincipalKey(e => e.Id); } }
数据库模型SQL
CREATE TABLE [Blogs] ( [Id] int NOT NULL, [Name] nvarchar(max) NULL, [SiteUri] nvarchar(max) NULL, CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) ); CREATE TABLE [Posts] ( [Id] int NOT NULL, [Title] nvarchar(max) NULL, [Content] nvarchar(max) NULL, [PublishedOn] datetime2 NOT NULL, [Archived] bit NOT NULL, [BlogId] int NOT NULL, CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]), CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]) ON DELETE CASCADE );
问题原因
EF Core与旧版EF的变更跟踪逻辑存在差异:第一次SaveChanges()后,父实体已持久化到数据库,此时通过导航属性添加新子实体时,EF Core会错误地尝试更新父实体(而非仅插入子实体)。但父实体在数据库中未发生任何修改,导致乐观并发检查失败,触发异常。
通用修复方案
方案1:关闭父实体的并发检查
如果业务不需要对父实体做乐观并发控制,可以在模型配置中关闭:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasMany(e => e.Posts) .WithOne(e => e.Blog) .HasForeignKey(e => e.BlogId) .HasPrincipalKey(e => e.Id); // 关闭Blog实体的并发令牌检查 modelBuilder.Entity<Blog>().Property(b => b.Id).IsConcurrencyToken(false); }
若需全局关闭并发警告,可在上下文配置中设置:
protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer("你的连接字符串") .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.DbUpdateConcurrencyWarning));
方案2:显式设置子实体状态
添加子实体后,手动将其状态标记为Added,避免EF Core误触发父实体更新:
blog.Posts.Add(post2); // 手动指定子实体为新增状态 db.Entry(post2).State = EntityState.Added; db.SaveChanges();
方案3:直接添加子实体到DbSet(已验证有效)
跳过导航属性,直接将子实体添加到对应的DbSet,EF Core会正确识别仅需插入子实体:
// blog.Posts.Add(post2); db.Posts.Add(post2); db.SaveChanges();
方案4:重新加载父实体
第一次SaveChanges()后重新加载父实体,确保上下文跟踪的状态与数据库一致:
db.SaveChanges(); // 重新加载父实体 db.Entry(blog).Reload(); var post2 = new Post { ... }; blog.Posts.Add(post2); db.SaveChanges();
内容的提问来源于stack exchange,提问作者Vladimir

