.net 具有SQLite一对多关系的EF6在没有外键的唯一约束上失败

rur96b6h  于 2023-11-20  发布在  .NET
关注(0)|答案(1)|浏览(202)

当使用Entity Framework 6在不使用外键的情况下将具有唯一约束的一对多关系的实体插入到SQLite数据库中时,我遇到了一个问题,并且我无法**改变这一点。让我们考虑这个例子:

型号

  1. public class Box {
  2. public int BoxId { get; set; }
  3. public ICollection<Item> Items{ get; set; }
  4. }
  5. public class Item {
  6. public int ItemId { get; set; }
  7. public Box Box { get; set; }
  8. }

字符串

第一次插入带有新框的项目

  1. Box box = new Box(){BoxId = 1};
  2. Item item = new Item(){ItemId = 1, Box = box};
  3. using (var db = new MyDbContext()) {
  4. db.Items.Add(item);
  5. db.SaveChanges();
  6. }


这将工作并创建Item和Box。
如果我再次运行与之前相同的代码,我会得到一个“unique constraint fail”错误,因为它不能创建另一个ID = 1的Box。所以解决方案是:

验证框

  1. Box box = new Box(){BoxId = 1};
  2. Item item = new Item(){ItemId = 1, Box = box};
  3. using (var db = new MyDbContext()) {
  4. var box = db.Box.FirstOrDefault(x => x.Id == box.Id);
  5. item.Box = box ?? item.Box;
  6. db.Items.Add(item);
  7. db.SaveChanges();
  8. }


我真的总是需要从数据库中获取盒子吗?如果是,我可以将对象保存在缓存中并重用它吗?我如何抽象获取盒子?我可以创建一个BoxService并在内部打开一个新的作用域上下文来获取盒子吗?
我在这里或多或少地添加了上下文和迁移,因为我的真实的场景涉及更多的属性,但为了简单起见,我只展示了这些模型。

移民

  1. migrationBuilder.CreateTable(
  2. name: "Boxs",
  3. columns: table => new
  4. {
  5. Id = table.Column<string>(type: "TEXT", nullable: false)
  6. },
  7. constraints: table =>
  8. {
  9. table.PrimaryKey("PK_Boxs", x => x.Id);
  10. });
  11. migrationBuilder.CreateTable(
  12. name: "Items",
  13. columns: table => new
  14. {
  15. ItemId = table.Column<string>(type: "TEXT", nullable: false)
  16. BoxId = table.Column<string>(type: "TEXT", nullable: false),
  17. },
  18. constraints: table =>
  19. {
  20. table.PrimaryKey("PK_Items", x => x.ItemId);
  21. table.ForeignKey(
  22. name: "FK_Items_Boxs_BoxId",
  23. column: x => x.BoxId,
  24. principalTable: "Boxs",
  25. principalColumn: "Id",
  26. onDelete: ReferentialAction.Cascade);
  27. });

背景

  1. public class ItemsDbContext : DbContext
  2. {
  3. public ItemsDbContext(DbContextOptions<ItemsDbContext> options) : base(options) { }
  4. public DbSet<Boxs> Boxs { get; set; }
  5. public DbSet<Items> Items { get; set; }
  6. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  7. {
  8. optionsBuilder.EnableSensitiveDataLogging();
  9. }
  10. protected override void OnModelCreating(ModelBuilder builder)
  11. {
  12. builder.Entity<Boxs>().HasMany(s => s.Items).WithOne(s => s.Box);
  13. }
  14. }

gywdnpxw

gywdnpxw1#

我真的总是需要从数据库中获取盒子吗?如果是,我可以将对象保存在缓存中并重用它吗?
不,你不需要,但你通常需要。这是一个关联问题。你想创建一个新的项目,但将其与现有的Box相关联。EF无法知道它所传递的Box引用是代表现有的Box还是新的Box,除非你告诉它。要在不获取Box的情况下执行您正在查找的操作:

  1. Box box = new Box(){BoxId = 1};
  2. Item item = new Item(){ItemId = 1, Box = box};
  3. using (var db = new MyDbContext()) {
  4. db.Attach(box); // <- Tells EF "treat this as an existing row"
  5. db.Items.Add(item);
  6. db.SaveChanges();
  7. }

字符串
这在本例中是可行的,因为我们定义了一个新的DbContext,它什么也不跟踪,只处理一个操作。如果你有一个类似于注入的DbContext示例,它 * 可能 * 已经跟踪了desured box(在另一个操作中加载,或被另一个项目引用等),那么你需要更多的工作来确保操作是安全的:

  1. // where _db is a module level reference or otherwise reused DbContext instance...
  2. Box box = _db.Boxes.Local.FirstOrDefault(x => x.BoxId == boxId); // <- Note .Local
  3. bool boxExists = box != null;
  4. if(!boxExists)
  5. {
  6. box = new Box(){BoxId = boxId};
  7. _db.Attach(box);
  8. }
  9. Item item = new Item(){ItemId = 1, Box = box};
  10. _db.Items.Add(item);
  11. _db.SaveChanges();
  12. if(!boxExists)
  13. _db.Entry(box).State = EntityState.Detached;


第一件事是检查本地跟踪缓存中是否存在Box。这不会转到数据库,它只是要求DbContext查找现有的跟踪实体。如果找到,我们使用它,否则我们创建Box并像第一个示例那样附加它。由于这个附加的引用现在被跟踪,在我们保存更改后,分离“stubbed”框是一个好主意,因为通常这不是一个 complete 框,只是一个存根,如果我们转到EF来获取一个框,我们需要一个完整的框,而不是一个仅具有ID集的跟踪存根。
我之所以说你应该只需要EF去数据库并获取盒子,是因为上面的逻辑很容易搞砸,导致情景运行时bug,当你希望引用一个现有的记录时,它也可以作为一个验证,在一个有效的状态下接收项目,这为您提供了一个有意义的验证点来Assert,而不是SaveChanges上的FK异常或更糟的情况,将项目保存在错误/无效的框中。

展开查看全部

相关问题