EF Core拥有的实体影子主键在SQLite中导致空约束冲突

4

我有一个属于评论的实体类型:

public class Comment {    // owned entity type
  public Comment(string text) { Text = text; }
  public string Text { get; private set; }
}

public class Post {
  public Post(string content) { Content = content; }
  public long Id { get; private set; }
  public string Content { get; private set; }
  public ICollection<Comment> Comments { get; private set; } = new HashSet<Comment>();
}

Post 的配置包括:

builder.OwnsMany(x => x.Comments, x => {
  x.Property(y => y.Text).IsRequired();
});

播种代码包括以下内容:
var post = new Post("content");
post.Comments.Add(new Comment("comment1"));
post.Comments.Add(new Comment("comment2"));
await _context.AddAsync(post);
await _context.SaveChangesAsync();

当我使用postgres提供程序时,我可以成功地创建、填充和编辑数据库。
当我使用sqlite提供程序时,我可以成功地创建数据库,但是当我尝试填充它时,我会收到以下错误信息:
Microsoft.EntityFrameworkCore.DbUpdateException: 更新条目时发生错误。有关详细信息,请参见内部异常。
---> Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite 错误 19:'NOT NULL constraint failed: Comment.Id'。 文档中提到所拥有的表具有隐式键,这解释了对Comment.Id的投诉。
但为什么只有在sqlite中会出现此问题,以及如何解决它?

1
我原本想说的是,通过在 GitHub 上搜索 / 发布问题,但看起来你已经这样做了(https://github.com/dotnet/efcore/issues/26521)。 - Jeremy Lakeman
1
@JeremyLakeman 如果它适用于Postgres但不适用于SQLite,对于完全相同的配置,则我认为这可能是一个错误...希望我错了,只是我需要一个解决方案! :-) - lonix
1个回答

10

这是由(1)不合适(据我看)的EF Core默认设置和(2)不支持的SQLite特性组合引起的。

  1. Collections of owned types的EF Core文档所解释

所有权类型需要主键。如果在.NET类型上没有好的候选属性,EF Core可以尝试创建一个。然而,当通过集合定义所有权类型时,仅创建一个影子属性作为所有者的外键和所有权实例的主键来充当它并不足够,就像我们对OwnsOne一样:每个所有者可能有多个所有权类型实例,因此所有者的键不足以为每个所有权实例提供唯一标识。

问题是,如果您没有定义显式的 PK,则 EF Core 会生成名为 Id 的影子属性(列),类型为int,自动增量(他们认为,但请参见 (2)),并在 (OwnerId, Id) 上定义复合 PK。

  1. 然而,SQLite 仅支持自增列作为单个 PK 列。因此,它生成常规的 INTId,需要在 INSERT 中显式指定值,但 EF Core 不会发送它,因为它仍然认为该属性是在服务器上自动生成的。

话虽如此,最好始终定义拥有集合实体的 PK。由于自增本身就是唯一的,所以最小的要求就是将自动生成的影子 Id 属性标记为 PK,例如:

builder.Entity<Post>.OwnsMany(e => e.Comments, cb => {
    cb.HasKey("Id"); // <-- add this
    // The rest...
    cb.Property(e => e.Text).IsRequired();
});

生成的迁移应该为 Id 列添加 "Sqlite:Autoincrement" 注释:
Id = table.Column<long>(type: "INTEGER", nullable: false)
    .Annotation("Sqlite:Autoincrement", true),

这是在 OP 设计中缺失的部分,导致了问题。

个人而言,我更喜欢 EF Core 抛出常规的未定义主键错误,而不是定义 PK 构造不受所有数据库支持。同时,SQLite 提供程序应该抛出异常,而不是默默地忽略自动增量模型请求,从而引入模型元数据之间的差异(EF Core 基础结构用于控制所有运行时行为)。因此,两者在技术上都可以被视为错误。但它们就是它们。通常情况下,优先使用约定而非配置,但对于具有任意默认值的事物要明确。


4
太棒了,我永远也不可能发现这一切。这对我帮助很大,谢谢!我还在repo问题中引用了你的评论,我希望他们能考虑你的想法——更清晰的异常信息将有助于我解决问题。 - lonix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接