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 一对多关系在唯一索引约束上失败的主要内容,如果未能解决你的问题,请参考以下文章

EF Core中通过Fluent API配置一对一关系

EF Core中通过Fluent API配置一对一关系

EF Core中通过Fluent API配置一对一关系

EF Core中通过Fluent API配置一对多关系

EF Core 一对一 一对多 多对多 关系定义

使用 Core Data 一对多关系从 JSON 数组填充 sqlite 数据库