在已保存实体的关系上添加新条目时,EF Core 不会检测到更改

Mus*_*gdy 5 c# change-tracking entity-framework-core

我有一个非常基本的设置来测试和理解为什么EF Core 6在将新项目添加到已保存实体的多端时默认不保存相关实体。

  1. 有人可以向我解释一下这个设置有什么问题吗?
  2. 如何让 EF 默认检测更改?我可以进行任何配置以使 EF 检测更改而无需entry手动设置状态吗?

我还遵循了Microsoft 文档中提供的示例,它给了我相同的结果 ( DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);)

using Microsoft.EntityFrameworkCore;

var db = new AuthorsDbContext();
db.Database.EnsureCreated();


var author = new Author();
author.Id = Guid.NewGuid();

db.Authors.Add(author);
db.SaveChanges();

// Not working
// var author1 = await db.Authors.FindAsync(new object?[] { author.Id }); 
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// Not working
// var author1 = await db.Authors.Include(a => a.Posts).FirstAsync(a => a.Id == author.Id);
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// Not working
// var author1 = await db.Authors.AsTracking().FirstAsync(a => a.Id == author.Id);
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// Not working
// var author1 = await db.Authors.Include(a=>a.Posts).AsTracking().FirstAsync(a => a.Id == author.Id);
// author1.Posts.Add(new Post() { Id = Guid.NewGuid(), Title = "test" });
// db.SaveChanges(); //-> DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s);

// this is the only way it works
// var author1 = await db.Authors.FindAsync(new object?[] { author.Id });
// var post = new Post() { Id = Guid.NewGuid(), Title = "test" };
// author1.Posts.Add(post);
// db.Entry(post).State = EntityState.Added;
// db.SaveChanges();

public class Author
{
  public Guid Id { get; set; }
  public List<Post> Posts { get; set; } = new();
}

public class Post
{
  public Guid Id { get; set; }
  public string Title { get; set; }
}

public class AuthorsDbContext : DbContext
{
  public DbSet<Author> Authors { get; set; }
  public DbSet<Post> Posts { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder.UseMySQL("Data Source=127.0.0.1;Initial Catalog=test-db-091;User Id=root;Password=DeV12345");
  }

protected override void OnModelCreating(ModelBuilder builder)
  {
    builder.Entity<Author>().HasKey(a => a.Id);
    builder.Entity<Post>().HasKey(a => a.Id);

    builder.Entity<Author>().HasMany(a => a.Posts).WithOne(a => a.Author).HasForeignKey(a => a.AuthorId);
    builder.Entity<Post>().HasOne(a => a.Author).WithMany().HasForeignKey(a => a.AuthorId);
  }
}
Run Code Online (Sandbox Code Playgroud)

Iva*_*oev 10

首先,发布的模型与流畅的配置不匹配,所以让我们纠正它。根据流畅的配置,Post具有引用导航属性和显式 FK 属性Author

public class Post
{
    //...
    public Author Author { get; set; }
    public Guid AuthorId { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

其次,这里存在关系配置错误

builder.Entity<Author>().HasMany(a => a.Posts).WithOne(a => a.Author).HasForeignKey(a => a.AuthorId);
builder.Entity<Post>().HasOne(a => a.Author).WithMany().HasForeignKey(a => a.AuthorId);

Run Code Online (Sandbox Code Playgroud)

您正在两次配置一个相同的关系,这通常是潜在问题的根源,因为在这种情况下,第二个配置(覆盖第一个)缺少 中的集合导航属性WithMany,这反过来会导致 2 个关系和 2 个关系FK。所以删除它并只留下第一个(正确的)

builder.Entity<Author>()
    .HasMany(a => a.Posts)
    .WithOne(a => a.Author)
    .HasForeignKey(a => a.AuthorId);
Run Code Online (Sandbox Code Playgroud)

通常,每个关系始终使用一种流畅的配置,并将导航属性传递给Has/With调用(当它们存在时)。


现在讨论主要主题 - 变更跟踪。当您直接调用更改跟踪 API(Entry.StateDbContext/ DbSet AddRemoveAttach)时,EF 仅使用您告诉它的任何内容以及所提供实体的状态。然而,当某个实体没有被跟踪,并且 EF 需要确定其状态(由其他几个地方ChangeTracker.DetectChanges调用SaveChanges)时,事情就会变得棘手,因为它不知道该实体是否是新的(因此应该添加)或现有且需要更新。

在这里,他们使用了一种适用于大多数情况的简单方法。如果PK是生成 或者设置了 PK(其值与 CLR 默认值不同),则实体被视为现有的,否则被视为新的

这就是这里问题的原因。Guid默认情况下,PK 被认为是自动生成的 ( ) ValueGeneratedOnAdd,并且由于您提供显式值,因此实例Post被认为是现有的,并且 EF 尝试更新(而不是插入),这当然会失败。

基本上有两种方法可以解决这个问题。第一种是使用默认的 PK 属性配置,并且不为新实体分配 PK 值(当实体进入更改跟踪器时,EF 会生成并分配一个值,但它也会知道它是自动生成的,并且实体必须是创建),即在所有“非工作”示例中,Id = Guid.NewGuid(),new Post() { ... }语句中删除,它们应该可以正常工作。

第二种方法是(如果您想使用显式GuidPK)保留用法不变,但更改流畅配置以告诉 EF 这些属性不是自动生成的,即

builder.Entity<Post>().Property(e => e.Id).ValueGeneratedNever();

Run Code Online (Sandbox Code Playgroud)