Entity Framework Core 2.2.4 ошибка при вставке элементов с отношением 1: n - лучшее решение - PullRequest
0 голосов
/ 08 мая 2019

У меня есть база данных SQL Server в качестве серверной части веб-службы и локальная база данных SQLite на мобильном интерфейсе.

Обе используют EF Core 2.2.4 с подходом TpH и разделяют большинствоклассов C #, но - конечно - у внутренних классов есть больше свойств из-за административных вещей, о которых нужно заботиться.Для моделирования различий используются условные атрибуты и свободный API (см. Код ниже).

Основная идея заключается в том, что содержимое базы данных создается на бэкэнде, а выдержка затем сохраняется в базе данных внешнего интерфейса.

В самом конце (и в упрощенном виде) локальная база данных должна хранить только список единиц.

Каждая единица содержит список упражнений (n: m).

Упражнения получены из абстрактного класса, и некоторые типы упражнений могут содержать список необходимых инструментов (1: n), которые хранятся в дополнительной таблице.

Каждое упражнение уникально, так что я могу использовать егопервичный ключ для отношения юнит-упражнение n: m.

С другой стороны, на инструмент может ссылаться более одного упражнения, что вызывает проблемы при добавлении юнитов с упражнениями в локальную базу данных внешнего интерфейса.

Проблема состоит в том, что в упражнении 1: n для отображения инструментов также используется первичный ключ инструмента, потому что именно так его создает бэкэнд, и, следовательно, если первичный ключ инструмента во внешнем интерфейсе уже существует,frontend db Add() вызов завершается неудачно с

Ошибка SQLite 19: «Не удалось выполнить ограничение UNIQUE: Tool.Id»

Итак, вопрос в том, что является самым простым способом.(пере) проектировать моdel для внешнего интерфейса, так что я только вставляю список единиц, сгенерированных бэкэндом

  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 голосов
/ 09 мая 2019

При работе с сущностями контекст является «владельцем» сущностей. Вы можете «создать» новую сущность, но вы должны учитывать, что контекст ожидает контроля над этой сущностью.

Когда вы делаете что-то вроде:

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 будет успешно добавлен, но parent2 не удастся. Это терпит неудачу, потому что EF по существу пройдет через оба случая и будет рассматривать ребенка каждого родителя как новую сущность. Когда он встречает «нового» дочернего элемента в Parent 1 с ID = 1, вы получаете нарушение PK.

Если вы настроите свои сущности на использование столбцов Identity, вы можете избежать этой ошибки, но дочерняя ссылка Родителя 1 и 2 будет указывать на две разные записи с разными сгенерированными идентификаторами.

Чтобы избежать этого, при настройке групповых данных сначала создайте и свяжите свои дочерние объекты, а затем свяжите их с родителями. Например:

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, а не ссылки, но возвращать эти сущности, которые будут использоваться как обычные сущности (при необходимости, отложенная загрузка ссылок), вам следует использовать DbSet.Create() вместо new.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...