EF Core / Sqlite 一对多关系在唯一索引约束上失败
Posted
技术标签:
【中文标题】EF Core / Sqlite 一对多关系在唯一索引约束上失败【英文标题】:EF Core / Sqlite one-to-many relationship failing on Unique Index Constraint 【发布时间】:2017-09-15 23:01:03 【问题描述】:上下文是每个Car
都有一个对应的CarBrand
。现在我的类如下图:
public class Car
public int CarId get; set;
public int CarBrandId get; set;
public CarBrand CarBrand get; set;
public class CarBrand
public int CarBrandId get; set;
public string Name get; set;
public class MyContext : DbContext
public DbSet<Car> Cars get; set;
public DbSet<CarBrand> CarBrands get; set;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
optionsBuilder.UseSqlite(@"Data Source = MyDatabase.sqlite");
这是我的代码的示例执行...
class Program
static void Main(string[] args)
AlwaysCreateNewDatabase();
//1st transaction
using (var context = new MyContext())
var honda = new CarBrand() Name = "Honda" ;
var car1 = new Car() CarBrand = honda ;
context.Cars.Add(car1);
context.SaveChanges();
//2nd transaction
using (var context = new MyContext())
var honda = GetCarBrand(1);
var car2 = new Car() CarBrand = honda ;
context.Cars.Add(car2);
context.SaveChanges(); // exception happens here...
static void AlwaysCreateNewDatabase()
using (var context = new MyContext())
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
static CarBrand GetCarBrand(int Id)
using (var context = new MyContext())
return context.CarBrands.Find(Id);
问题是当car2
使用相同的CarBrand
honda
添加到数据库时,我得到'UNIQUE constraint failed: CarBrands.CarBrandId' 异常。
我希望它在第二个事务的context.SaveChanges()
中做的是,它会添加car2
并适当地设置它与CarBrand
的关系,但我得到了一个异常。
编辑:我真的需要在不同的上下文/事务中获取我的 CarBrand 实例。
//really need to get CarBrand instance from different context/transaction
CarBrand hondaDb = null;
using (var context = new MyContext())
hondaDb = context.CarBrands.First(x => x.Name == "Honda");
//2nd transaction
using (var context = new MyContext())
var car2 = new Car() CarBrand = hondaDb ;
context.Cars.Add(car2);
context.SaveChanges(); // exception happens here...
【问题讨论】:
不确定这是否是导致问题的原因,但我通常在 CRUD 操作中看不到静态。 为什么每次都必须使用不同的上下文? 假设我重新启动了应用程序。那么这将是在不同的情况下,对吧? 【参考方案1】:问题在于Add
方法级联:
在
Added
状态下开始跟踪给定实体,和任何其他尚未被跟踪的可访问实体,以便在调用SaveChanges
时将它们插入到数据库中。
实现目标的方法有很多,但最灵活(我猜是首选)是将Add
方法调用替换为ChangeTracker.TrackGraph
方法:
开始跟踪实体以及通过遍历其导航属性可到达的任何实体。遍历是递归的,因此任何已发现实体的导航属性也将被扫描。为每个发现的实体调用指定的回调,并且必须设置每个实体应该被跟踪的状态。如果没有设置状态,实体将保持未跟踪状态。 此方法设计用于断开连接的场景,其中使用上下文的一个实例检索实体,然后使用上下文的不同实例保存更改。这样的示例是一个 Web 服务,其中一个服务调用从数据库中检索实体,另一个服务调用保留对实体的任何更改。每个服务调用都使用调用完成时释放的上下文的一个新实例。 如果发现已被上下文跟踪的实体,则不处理该实体(并且不遍历其导航属性)。
因此,您可以使用以下代码而不是 context.Cars.Add(car2);
(它非常通用,几乎适用于所有场景):
context.ChangeTracker.TrackGraph(car2, node =>
node.Entry.State = !node.Entry.IsKeySet ? EntityState.Added : EntityState.Unchanged);
【讨论】:
【参考方案2】:快速修复:
EntityFramework Core 使用新的Context
在使用另一个代码时存在问题。您在它们两者之间混合了实体的状态。
只需使用相同的上下文来获取和创建。
如果你选择:
using (var context = new MyContext())
var honda = context.CarBrands.Find(1);
var car2 = new Car() CarBrand = honda ;
context.Cars.Add(car2);
context.SaveChanges(); //fine now
var cars = context.Cars.ToList(); //will get two
应该没问题。
如果你真的想要两个上下文
static void Main(string[] args)
AlwaysCreateNewDatabase();
CarBrand hondaDb = null;
using (var context = new MyContext())
hondaDb = context.CarBrands.Add(new CarBrand Name = "Honda").Entity;
context.SaveChanges();
using (var context = new MyContext())
var car2 = new Car() CarBrandId = hondaDb.CarBrandId ;
context.Cars.Add(car2);
context.SaveChanges();
您现在不依赖更改跟踪机制,而是依赖基于 CarBrandId
的普通 FOREIGN KEY 关系。
原因:更改跟踪
错误可能会产生误导,但如果您查看上下文的 StateManager
,您会发现原因。
因为您从另一个Context
获取的CarBrand
是您正在使用的,对于Context
的另一个实例,它看起来像一个新实例。
您可以在此特定数据库上下文中使用该特定实体的属性 EntityState 在调试中清楚地看到它:
它说 CarBrand 现在正被添加到带有 Id: 1
的数据库中,这就是 CarBrand 表的 PRIMARY KEY 的唯一约束被破坏了。
错误说它的CarBrandId
打破了约束,因为您通过CarBrandId
支持的关系 Car-CarBrand 强制在数据库中创建新记录。
在您用于查询数据库的上下文中,带有 Id: 1
的 CarBrand 会是什么状态? 不变当然。但这是唯一知道它的Context
:
如果您想深入了解该主题,请在此处阅读 EF Core 中的 Change Tracking
概念:https://docs.microsoft.com/en-us/ef/core/querying/tracking
【讨论】:
如果我真的需要在不同的上下文/事务中获取 CarBrand 实例怎么办? @PaoloGo 你应该看看存储库模式 如果你真的想要单独的上下文,还有另一个补充(我确实意识到你可能试图想象比这里显示的更广泛的想法)。【参考方案3】:你不应该设置 CarBrand var car2 = new Car() CarBrand = honda ;
你应该设置CarBrandId
:var car2 = new Car() CarBrandId = honda.CarBrandId ;
你“可以”像这样构建模型:
modelBuilder
.Entity<Car>()
.HasOne(c => c.CarBrand)
.WithMany(cb => cb.Cars)
.HasForeignKey(c => c.CarBrandId)
.HasConstraintName("FK_Car_CarBrand")
.IsRequired();
【讨论】:
以上是关于EF Core / Sqlite 一对多关系在唯一索引约束上失败的主要内容,如果未能解决你的问题,请参考以下文章