2

我试图弄清楚如何解决一些导致 SQL 中重复行的争用问题。下面我展示了代码,在最底部展示了我的问题/疑虑。

此处提供 VS2012 解决方案的完整下载:http ://www15.zippyshare.com/d/72956037/4552733/EFConcurrency.zip

我使用设计器创建了一个实体框架模型。这是模型:

在此处输入图像描述

我已经使用以下代码填充了数据库:

        using (EfTestContainer db = new EfTestContainer())
        {
            User u = new User();
            u.Email = "test@test.com";
            db.Users.Add(u);
            for (int i = 1; i < 10000; i++)
            {
                Car c = new Car();
                c.CarName = "Cool Car";
                c.User = u;
                db.Cars.Add(c);
            }
            db.SaveChanges();
        }

如您所见,我们的数据库现在有 1 个用户和 10,000 辆汽车。

然后我创建三个相互竞争的线程来添加 SpareTires。

    private void button1_Click(object sender, EventArgs e)
    {
        // This will start three threads that all try to add SpareTires at the same time.
        ThreadPool.QueueUserWorkItem(new WaitCallback(Go), new object());

    }
    public static void Go(object o)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddSpareTires2), new object());
        Thread.Sleep(50);
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddSpareTires3), new object());
        Thread.Sleep(500);
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddSpareTires1), new object());
        while (true)
        {
            if (done1 && done2 && done3) break;
            Thread.Sleep(250);
        }
        MessageBox.Show("Done!");

    }
    public static bool done1 = false;
    public static bool done2 = false;
    public static bool done3 = false;
    private static void AddSpareTires1(object o)
    {
        using (EfTestContainer db = new EfTestContainer())
        {
            User u = db.Users.FirstOrDefault();
            var cars = db.Cars.ToList<Car>();
            foreach (var car in cars)
            {
                SpareTire st = new SpareTire();
                st.BrandName = "Cool Tire";
                car.SpareTire = st;
            }
            try
            {
                db.SaveChanges();
            }
            catch (OptimisticConcurrencyException exc)
            {
                var objectContext = ((IObjectContextAdapter)db).ObjectContext;
                objectContext.Refresh(RefreshMode.StoreWins, cars); // It doesn't seem to make a difference if I use RefreshMode.ClientWins
                db.SaveChanges();
            }
            catch (Exception)
            {

            }
            done1 = true;
        }
    }
    private static void AddSpareTires2(object o)
    {
        using (EfTestContainer db = new EfTestContainer())
        {
            for (int i = 1; i < 100; i++)
            {
                int min = (i-1) * 10;
                int max = i * 10;
                User u = db.Users.FirstOrDefault();
                var cars = db.Cars.Where(c => c.Id > min && c.Id < max).ToList<Car>();
                foreach (var car in cars)
                {
                    SpareTire st = new SpareTire();
                    st.BrandName = "Cool Tire";
                    car.SpareTire = st;

                }
                try
                {
                    db.SaveChanges();
                }
                catch (OptimisticConcurrencyException exc)
                {
                    var objectContext = ((IObjectContextAdapter)db).ObjectContext;
                    objectContext.Refresh(RefreshMode.StoreWins, cars); // It doesn't seem to make a difference if I use RefreshMode.ClientWins
                    db.SaveChanges();
                }
                catch (Exception)
                {

                }
            }
            done2 = true;

        }
    }
    private static void AddSpareTires3(object o)
    {
        using (EfTestContainer db = new EfTestContainer())
        {
            for (int i = 1; i < 10; i++)
            {
                int min = (i - 1) * 100;
                int max = i * 100;
                User u = db.Users.FirstOrDefault();
                var cars = db.Cars.Where(c => c.Id > min && c.Id < max).ToList<Car>();
                foreach (var car in cars)
                {
                    SpareTire st = new SpareTire();
                    st.BrandName = "Cool Tire";
                    car.SpareTire = st;
                }
                try
                {
                    db.SaveChanges();
                }
                catch (OptimisticConcurrencyException exc)
                {
                    var objectContext = ((IObjectContextAdapter)db).ObjectContext;
                    objectContext.Refresh(RefreshMode.StoreWins, cars); // It doesn't seem to make a difference if I use RefreshMode.ClientWins
                    db.SaveChanges();
                }
                catch (Exception)
                {

                }
            }
            done3 = true;
        }
    }

结果是有 1 个用户、10,000 辆汽车和 10,899 个备用轮胎!

  1. 为什么 SQL/EF 允许违反 Car / SpareTire 1 到(零或 1)关系?
  2. 如何修复我的代码,以便在运行后只存在 1 个用户、10,000 辆汽车和 10,000 个备用轮胎?

非常感谢!

4

1 回答 1

2

当您使用Model-First Entity Framework创建一对一关系时,实际上会将其映射到数据库端的一对多关系。您可以在 xml 编辑器中打开 edmx 文件时看到(您也可以在.Store设计器模型浏览器的部分中找到它)。在该<edmx:StorageModels>部分中,您会发现:

<Association Name="CarSpareTire">
  <End Role="Car" Type="EfTest.Store.Cars" Multiplicity="1" />
  <End Role="SpareTire" Type="EfTest.Store.SpareTires" Multiplicity="*" />
  <ReferentialConstraint>
    <Principal Role="Car">
      <PropertyRef Name="Id" />
    </Principal>
    <Dependent Role="SpareTire">
      <PropertyRef Name="Car_Id" />
    </Dependent>
  </ReferentialConstraint>
</Association>

多重性是*一方面,外键是一个单独的键Car_Id,它是数据库列,但不是模型中的属性。在该<edmx:ConceptualModels>部分中,关联被定义为一对零..一:

<Association Name="CarSpareTire">
  <End Type="EfTest.Car" Role="Car" Multiplicity="1" />
  <End Type="EfTest.SpareTire" Role="SpareTire" Multiplicity="0..1" />
</Association>

结果,数据库不知道一对一的关系,并且允许为单个存储多个SpareTires (具有相同的Car_Id值)Car(就好像Car实际上有一个SpareTires 集合而不是单个引用一样)。

我不知道为什么 EF 确实以这种方式将关系映射到数据库,以及它是有意的还是错误的。但是在您的多线程场景中,它无法工作,并且您将在数据库中获得多个SpareTires Car(根据概念模型这是错误的)。

我看到了解决问题的三个选项:

  • 使用代码优先而不是模型优先。Code-First 使用共享主键将一对零..one 关系(一侧可选,另一侧必需)映射到数据库中真正的一对零..one 关系,即Car_Id除了主键SpareTire同时是外键Car。使用共享主键,数据库不能为同一主体存储多个依赖项,并且您的问题不会发生。

  • 按照这种方法使用 Model-First 创建与共享主键的一对零关系。实际上,Model-First 似乎是可能的,但它不是默认设置。

  • 向列添加唯一约束Car_Id(手动在数据库中或在 Model-First 创建的 SQL 脚本中)

在所有情况下,当两个线程尝试SpareTire为相同的Car. 我不确定是否OptimisticConcurrencyExceptions。它们也可能是指示重复主键的其他异常类型,因此您可以在空块中放入一些内容,以便catch (Exception) { }至少在输入它们时通知您。

于 2013-07-30T23:46:13.067 回答