具有1:n关系的插入项时出现Entity Framework Core 2.2.4错误-最佳解决方案

时间:2019-05-08 19:50:51

标签: c# .net entity-framework xamarin.android

我有一个SQL Server数据库作为Web服务后端,在移动前端有一个本地SQLite数据库。

两者都使用带有 TpH 方法的EF Core 2.2.4并共享大多数C#类,但是-当然-后端类由于需要管理的原因而具有更多的属性。为了对差异进行建模,使用了条件属性和流畅的API(请参见下面的代码)。

主要思想是在后端创建数据库内容,然后将摘录存储在前端数据库中。

到最后(简化),本地数据库应仅存储单元列表。

每个单元包含一个练习列表(n:m)。

练习是从抽象类派生的,某些练习类型可以包含存储在额外表中的所需工具(1:n)列表。

每次练习都是唯一的,因此我可以将其主键用于n:m单元练习关系。

另一方面,一个工具可能被多个练习引用,这给将带有练习的单元添加到本地前端数据库带来了麻烦。

问题在于,练习工具1:n映射也使用该工具的主键,因为这是后端创建它的方式,因此,如果前端上的工具的主键已经存在,则前端数据库Add()调用失败,并显示

  

SQLite错误19:“唯一约束失败:Tool.Id”

所以,问题是:(重新)设计前端模型的最简单方法是什么,以便我仅插入后端生成的单元列表

  1. 没有错误
  2. 没有手动做很多数据库工作,这就是EF应该为我做的

下面是示例代码

namespace Test
{
#if FRONTEND
    [Table("TestUnit")]
#endif      
    public class TestUnit
    {
        public Guid Id { get; set; } // Use values from backend

        public string Name { get; set; }

        public List<TestExerciseSequence> ExerciseSequences { get; set; }
    }

    public class TestExerciseSequence
    {
        public Guid UnitId { get; set; }
        public TestUnit Unit { get; set; }

        public Guid ExerciseId { get; set; }
        public TestExercise Exercise { get; set; }
    }


#if FRONTEND
    [Table("TestExercise")]
#endif
    public abstract class TestExercise
    {
        public Guid Id { get; set; }

        public string Name { get; set; }

        public string Discriminator { get; set; }

        public List<TestExerciseSequence> ExerciseSequences { get; set; }
    }

    public class TestExerciseType1 : TestExercise
    {
        public TestExerciseToolType ToolType { get; set; }
    }

    public class TestExerciseToolType
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public List<TestExerciseType1> ExerciseTypes { get; set; }
    }

    public class TestLocalDBContext : DbContext
    {
        // All locally stored units
        public DbSet<TestUnit> Units { get; set; }

        private const string databaseName = "sqlite.db";
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            string databasePath;
            switch (Device.RuntimePlatform)
            {
                case Device.Android:
                    databasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), databaseName);
                    break;
                default:
                    throw new NotImplementedException("Platform not supported");
            }
#if FRONTEND                        
            optionsBuilder.UseSqlite($"Filename={databasePath}")
                          .EnableDetailedErrors()
                          .EnableSensitiveDataLogging()
                          ;
#endif                                                  
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // n : m relationship between 'Unit' to 'Exercise'
            modelBuilder.Entity<TestExerciseSequence>()
                .HasKey(us => new { us.UnitId, us.ExerciseId });

            modelBuilder.Entity<TestExerciseSequence>()
                .HasOne<TestUnit>(us => us.Unit)
                .WithMany(u => u.ExerciseSequences)
                .HasForeignKey(us => us.UnitId);

            modelBuilder.Entity<TestExerciseSequence>()
                .HasOne<TestExercise>(us => us.Exercise)
                .WithMany(e => e.ExerciseSequences)
                .HasForeignKey(us => us.ExerciseId);

#if FRONTEND                                
            // TestExerciseToolType causes troubles     
            modelBuilder.Entity<TestExerciseToolType>()
                .Property(c => c.Id)
                .ValueGeneratedNever();

            modelBuilder.Entity<TestExerciseToolType>()
                .HasIndex(c => c.Id)
                .IsUnique(true);
#endif

            // 1 : n relationship between 'TestExerciseType1' to 'TestExerciseToolType'
            modelBuilder.Entity<TestExerciseType1>()
                .HasOne<TestExerciseToolType>(etmt => etmt.ToolType)
                .WithMany(etmt => etmt.ExerciseTypes);

#if FRONTEND                                
            // Copied from above        
            modelBuilder.Entity<TestExerciseToolType>()
                .Property(c => c.Id)
                .ValueGeneratedNever();
            modelBuilder.Entity<TestExerciseToolType>()
                .HasIndex(c => c.Id)
                .IsUnique(true);
#endif

            // Table-per-Hierarchy for 'Exercise'
            modelBuilder.Entity<TestExercise>()
                .HasDiscriminator<string>("Discriminator")
                .HasValue<TestExerciseType1>("ExerciseTypeMovement")
                                ;

            modelBuilder.Entity<TestExercise>().Property("Discriminator").HasMaxLength(80);

            // Unit - ExerciseSequence
            modelBuilder.Entity<TestUnit>().HasMany(u => u.ExerciseSequences);
        }
    }
    public class TestDBHelper<T> where T : TestLocalDBContext
    {
        protected TestLocalDBContext CreateContext()
        {
            var dbContext = (T)Activator.CreateInstance(typeof(T));
            dbContext.Database.EnsureCreated();
            dbContext.Database.Migrate();
            return dbContext;
        }

        public void AddUnit(TestUnit u)
        {
            using (var context = CreateContext())
            {
                context.Units.Add(u);
                context.SaveChanges();
            }
        }
    }
}

和测试代码

    private void DoTest()
    {
            var uId1 = Guid.NewGuid();
            var uId2 = Guid.NewGuid();

            var eId1 = Guid.NewGuid();
            var eId2 = Guid.NewGuid();
            var eId3 = Guid.NewGuid();
            var eId4 = Guid.NewGuid();

            var u1 = new TestUnit()
            {
                    Id = uId1,
                    Name = "Unit1",
                    ExerciseSequences = new List<TestExerciseSequence>()
                    {
                            new TestExerciseSequence()
                            {
                                    UnitId = uId1,
                                    ExerciseId = eId1,
                                    Exercise = new TestExerciseType1()
                                    {
                                            Id = eId1,
                                            Discriminator = "TestExerciseType1",
                                            Name = "E1",
                                            ToolType = new TestExerciseToolType()
                                            {
                                                    Id = 1,
                                                    Name = "M1"
                                            }
                                    }
                            },
                            new TestExerciseSequence()
                            {
                                    UnitId = uId1,
                                    ExerciseId = eId2,
                                    Exercise = new TestExerciseType1()
                                    {
                                            Id = eId2,
                                            Discriminator = "TestExerciseType1",
                                            Name = "E2",
                                            ToolType = new TestExerciseToolType()
                                            {
                                                    Id = 2,
                                                    Name = "M2"
                                            }
                                    }
                            }
                    }
            };

            var u2 = new TestUnit()
            {
                    Id = uId2,
                    Name = "Unit2",
                    ExerciseSequences = new List<TestExerciseSequence>()
                    {
                            new TestExerciseSequence()
                            {
                                    UnitId = uId2,
                                    ExerciseId = eId3,
                                    Exercise = new TestExerciseType1()
                                    {
                                            Id = eId3,
                                            Discriminator = "TestExerciseType1",
                                            Name = "E3",
                                            ToolType = new TestExerciseToolType()
                                            {
                                                    Id = 3,
                                                    Name = "M3"
                                            }
                                    }
                            },
                            new TestExerciseSequence()
                            {
                                    UnitId = uId2,
                                    ExerciseId = eId4,
                                    Exercise = new TestExerciseType1()
                                    {
                                            Id = eId4,
                                            Discriminator = "TestExerciseType1",
                                            Name = "E4",
                                            ToolType = new TestExerciseToolType()
                                            {
                                                    Id = 1, // Exception!
                                                    Name = "M1"
                                            }
                                    }
                            }
                    }
            };

            try
            {
                    var database = new TestDBHelper<TestLocalDBContext>();
                    database.AddUnit(u1);
                    database.AddUnit(u2); // Exception 
            }
            catch (Exception ex)
            {
                    Debug.WriteLine($"Error {ex.Message}");
            }
    }

预先感谢

1 个答案:

答案 0 :(得分:0)

使用实体时,上下文是实体的“所有者”。您可以“新建”一个实体,但是您必须尊重上下文期望由该实体控制。

当您执行以下操作时:

var parent1 = new Parent
{
   ParentId = 1,
   Child = new Child
   {
      ChildId = 1,
   },
}
var parent2 = new Parent
{
   ParentId = 2,
   Child = new Child
   {
      ChildId = 1,
   }
}
context.Parents.Add(parent1);
context.Parents.Add(parent2);
context.SaveChanges();

父级1将成功添加,但父级2将失败。之所以失败,是因为EF本质上会同时处理这两种情况,并将每个父母的孩子视为一个新实体。当它在父代1上遇到ID为1的“新”孩子时,您会遇到PK违规。

如果将实体设置为使用“身份”列,则可以避免此错误,但是父级1和2的子级引用将指向两个具有不同生成ID的不同记录。

为避免这种情况,在设置批量数据时,请先创建并关联您的子实体,然后再将其与父实体关联。例如:

var child1 = new Child { ChildId = 1 };
var parent1 = new Parent 
{
  ParentId = 1,
  Child = child1
}
var parent2 = new Parent
{
  ParentId = 2,
  Child = child1
}
context.Parents.Add(parent1);
context.Parents.Add(parent2);
context.SaveChanges();

这样,父母双方都会引用同一个孩子,并且随着EF继续保留每个父母,它将从上下文中解析相同的孩子引用。

一对多关系也是如此: parent1.Children.Add(child1) 而不是 parent1.Children.Add(new Child { ChildId = 1 });

如果您要创建实体并设置FK ID而不是引用,但是将那些实体作为普通实体使用(如果需要,请延迟加载引用),那么您应该使用DbSet.Create()而不是new