Ядро EF — ошибка с дублированным ключом при добавлении данных с уникальным индексом

#c# #entity-framework #ef-code-first #entity-framework-core #ef-code-first-mapping

#c# #entity-framework #ef-code-first #entity-framework-core #ef-code-first-mapping

Вопрос:

Я пытаюсь добавить данные в базу данных с помощью ядра EF, но я не могу преодолеть ошибку с дублированным ключом: Cannot insert duplicate key row in object 'dbo.Stocks' with unique index 'IX_Stocks_Name'. The duplicate key value is (stock1).

У меня есть объект, Stock который имеет отношение один ко многим с Transaction — один Stock может быть связан со многими Transaction .

Проблема возникает не только в том случае, если в базе данных нет Stock с заданным идентификатором — в этом случае я могу добавить много Transaction файлов с одинаковым StockID значением, и это работает:

 using(var context = dbContextFactory.CreateDbContext(new string[0]))
     {
        List<Transaction> list = new List<Transaction>();
        //If there's no Stock with ID "stock1" in the database the code works.
        //Executing the code for the second time results in error, however, if I changed stock1 to stock2 it would work 
        var stock1 = new Stock("stock1");
        list.AddRange(new Transaction[]
        {
        //I can add two exactly the same `Transaction`s and it's ok when executed on a fresh database
        new Transaction(stock1, DateTime.Now, 1M, 5),
        new Transaction(stock1, DateTime.Now, 1M, 5),
        });
        context.Transactions.AddRange(list);
        context.SaveChanges();
     }
  

Ниже я добавил определения классов с их IEntityTypeConfiguration именами.

Заранее спасибо за любые подсказки.

Transaction класс:

 public class Transaction
    {
        public Stock RelatedStock { get; set; }
        public DateTime TransactionTime { get; set; }
        public Decimal Price { get; set; }
        public int Volume { get; set; }
        public Transaction() { }
    }
  

TransactionConfiguration класс:

 public class TransactionConfiguration : IEntityTypeConfiguration<Transaction>
    {
        public void Configure(EntityTypeBuilder<Transaction> builder)
        {
            builder
                .ToTable("Transactions");

            builder
                .Property<int>("TransactionID")
                .HasColumnType("int")
                .ValueGeneratedOnAdd()
                .HasAnnotation("Key", 0);

            builder
                .Property(transaction => transaction.Price)
                .HasColumnName("Price")
                .IsRequired();

            builder
                .Property(transaction => transaction.TransactionTime)
                .HasColumnName("Time")
                .IsRequired();

            builder
                .Property(transaction => transaction.Volume)
                .HasColumnName("Volume")
                .IsRequired();

            builder
                .HasIndex("RelatedStockStockID", nameof(Transaction.TransactionTime))
                .IsUnique();
        }
    }
  

Stock класс:

 public class Stock
  {
     public string Name { get; set; }
     public ICollection<Transaction> Transactions { get; set; }
     public Stock() { }
  }
  

StockConfiguration класс:

 public class StockConfiguration : IEntityTypeConfiguration<Stock>
    {
        public void Configure(EntityTypeBuilder<Stock> builder)
        {
            builder
                .ToTable("Stocks");

            builder
                .Property<int>("StockID")
                .HasColumnType("int")
                .ValueGeneratedOnAdd()
                .HasAnnotation("Key", 0);

            builder
                .Property(stock => stock.Name)
                .HasColumnName("Name")
                .HasMaxLength(25)
                .IsRequired();

            builder
                .HasMany(stock => stock.Transactions)
                .WithOne(transaction => transaction.RelatedStock)
                .IsRequired();

            builder
                .HasIndex(stock => stock.Name)
                .IsUnique();
        }
    }
  

Ответ №1:

В dbo.Stocks таблице с именем IX_Stocks_Name есть уникальный индекс. Вы нарушаете этот индекс.

Ваша проблема в этой строке:

 var stock1 = new Stock("stock1");
  

Вы создаете «stock1» снова и снова. Вместо этого вам следует сначала получить (или сохранить) Stock объект для «stock1» и использовать его, если он уже существует. Если он не существует, то его безопасно создать.

Короче говоря, код выполняет INSERT преобразование dbo.Stocks с существующим Name .

Комментарии:

1. Что касается упомянутой вами строки — я создавал Stock вручную ради описания проблемы. На самом деле, я анализирую текстовый файл и передаю List<Transaction> в функцию, а затем вызываю context.Transactions.AddRange(list) Конечно, на каждую акцию ссылается множество транзакций. Не могли бы вы подробнее рассказать о возможном подходе к решению проблемы?

Ответ №2:

Вам нужно проверить, есть ли уже известный запас с желаемым идентификатором. ради вашего примера:

 var stock1 = context.Stocks.SingleOrDefault(x => x.StockId == "stock1") ?? new Stock("stock1");
  

Это свяжет существующий запас, если он уже есть в базе данных, или свяжет новый, если его нет.

Комментарии:

1. Спасибо за подсказку. Честно говоря, я ожидал бы, что Entity Framework позаботится о связывании объектов таким образом автоматически.

2. Не имело бы смысла делать это автоматически. Используйте автоматически сгенерированные цифровые ключи или ключи guid, чтобы избежать этой проблемы.

Ответ №3:

Благодаря предложениям @Zer0 и @Steve Py я пришел к следующему решению:

 void Insert(IEnumerable<Transaction> data)
        {
            using(var context = dbContextFactory.CreateDbContext(new string[0]))
            {
                List<Stock> stocks = data.Select(s => s.RelatedStock).Distinct(new StockComparer()).ToList();
                context.AddRange(stocks);
                context.SaveChanges();
                stocks = context.Stocks.ToList();

                List<Transaction> newList = new List<Transaction>(data.Count());

                foreach (var t in data)
                {
                        Stock relatedStock = stocks.Where(s => s.Name == t.RelatedStock.Name).First();
                        t.RelatedStock = relatedStock;
                        newList.Add(t);
                }

                context.Transactions.AddRange(newList);
                context.SaveChanges();
            }
        }