【问题标题】:EF Core / Sqlite one-to-many relationship failing on Unique Index ConstraintEF Core / Sqlite 一对多关系在唯一索引约束上失败
【发布时间】: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 操作中看不到静态。
  • 为什么每次都必须使用不同的上下文?
  • 假设我重新启动了应用程序。那么这将是在不同的情况下,对吧?

标签: c# sql entity-framework sqlite entity-framework-core


【解决方案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();
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-10-18
      • 1970-01-01
      • 2017-04-07
      • 1970-01-01
      • 1970-01-01
      • 2018-09-14
      • 2021-11-02
      • 2021-10-06
      相关资源
      最近更新 更多