#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();
}
}