Обходное решение см. В разделе «Обходное решение» ниже.
Какова ваша конкретная c причина использования DbSet.Update ? У него особая цель, связанная с отслеживанием.
Вместо этого должно быть достаточно удалить старые элементы и добавить новые:
model.Items.Remove(someOldItem); // or use other `Remove` methods
model.Items.AddRange(newItems);
context.SaveChanges();
Вот полный пример того, как это работает:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public ICollection<IceCreamVariation> Variations { get; set; } = new HashSet<IceCreamVariation>();
}
public class IceCreamVariation
{
public int IceCreamVariationId { get; set; }
public string Name { get; set; }
public int IceCreamId { get; set; }
public IceCream IceCream { get; set; }
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
public DbSet<IceCreamVariation> IceCreamVariations { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMySql(
"server=127.0.0.1;port=3306;user=root;password=;database=So61383388",
b => b.ServerVersion("8.0.20-mysql"))
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>()
.HasData(
new IceCream {IceCreamId = 1, Name = "Vanilla"},
new IceCream {IceCreamId = 2, Name = "Chocolate"}
);
modelBuilder.Entity<IceCreamVariation>()
.HasData(
new IceCreamVariation {IceCreamVariationId = 1, Name = "Double Vanilla Bourbon", IceCreamId = 1},
new IceCreamVariation {IceCreamVariationId = 2, Name = "Vanilla Caramel", IceCreamId = 1},
new IceCreamVariation {IceCreamVariationId = 3, Name = "Chocolate Hazelnut", IceCreamId = 2}
);
}
}
internal class Program
{
private static void Main()
{
using (var context = new Context())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var iceCreamsWithOldVariations = context.IceCreams
.Include(i => i.Variations)
.OrderBy(i => i.IceCreamId)
.ToList();
Debug.Assert(iceCreamsWithOldVariations.Count == 2);
Debug.Assert(iceCreamsWithOldVariations[0].Variations.Count == 2);
Debug.Assert(iceCreamsWithOldVariations[1].Variations.Count == 1);
var vanillaIceCream = iceCreamsWithOldVariations[0];
var vanillaCaramelVariation = iceCreamsWithOldVariations[0].Variations.First();
vanillaIceCream.Variations.Remove(vanillaCaramelVariation);
vanillaIceCream.Variations.Add(new IceCreamVariation {Name = "Vanilla Cheesecake"});
vanillaIceCream.Variations.Add(new IceCreamVariation {Name = "Vanilla-Lemon"});
var cholocateIceCream = iceCreamsWithOldVariations[1];
cholocateIceCream.Variations.Clear();
cholocateIceCream.Variations.Add(new IceCreamVariation {Name = "Chocolate Fudge Brownie"});
cholocateIceCream.Variations.Add(new IceCreamVariation {Name = "Chocolate-Peanut Butter"});
context.SaveChanges();
var iceCreamsWithNewVariations = context.IceCreams
.Include(i => i.Variations)
.OrderBy(i => i.IceCreamId)
.ToList();
Debug.Assert(iceCreamsWithNewVariations.Count == 2);
Debug.Assert(iceCreamsWithNewVariations[0].Variations.Count == 3);
Debug.Assert(iceCreamsWithNewVariations[1].Variations.Count == 2);
}
}
}
}
Следующие SQL операторы генерируются, которые работают как ожидалось:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE DATABASE `So61383388`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (38ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE `IceCreams` (
`IceCreamId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (35ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE `IceCreamVariations` (
`IceCreamVariationId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
`IceCreamId` int NOT NULL,
CONSTRAINT `PK_IceCreamVariations` PRIMARY KEY (`IceCreamVariationId`),
CONSTRAINT `FK_IceCreamVariations_IceCreams_IceCreamId` FOREIGN KEY (`IceCreamId`) REFERENCES `IceCreams` (`IceCreamId`) ON DELETE CASCADE
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Name`)
VALUES (1, 'Vanilla');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Name`)
VALUES (2, 'Chocolate');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreamVariations` (`IceCreamVariationId`, `IceCreamId`, `Name`)
VALUES (1, 1, 'Double Vanilla Bourbon');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreamVariations` (`IceCreamVariationId`, `IceCreamId`, `Name`)
VALUES (2, 1, 'Vanilla Caramel');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreamVariations` (`IceCreamVariationId`, `IceCreamId`, `Name`)
VALUES (3, 2, 'Chocolate Hazelnut');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (30ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE INDEX `IX_IceCreamVariations_IceCreamId` ON `IceCreamVariations` (`IceCreamId`);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `i`.`IceCreamId`, `i`.`Name`, `i0`.`IceCreamVariationId`, `i0`.`IceCreamId`, `i0`.`Name`
FROM `IceCreams` AS `i`
LEFT JOIN `IceCreamVariations` AS `i0` ON `i`.`IceCreamId` = `i0`.`IceCreamId`
ORDER BY `i`.`IceCreamId`, `i0`.`IceCreamVariationId`
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (7ms) [Parameters=[@p0='1', @p1='3', @p2='1', @p3='Vanilla Cheesecake' (Size = 4000), @p4='1', @p5='Vanilla-Lemon' (Size = 4000), @p6='2', @p7='Chocolate Fudge Brownie' (Size = 4000), @p8='2', @p9='Chocolate-Peanut Butter' (Size = 4000)], CommandType='Text', CommandTimeout='30']
DELETE FROM `IceCreamVariations`
WHERE `IceCreamVariationId` = @p0;
SELECT ROW_COUNT();
DELETE FROM `IceCreamVariations`
WHERE `IceCreamVariationId` = @p1;
SELECT ROW_COUNT();
INSERT INTO `IceCreamVariations` (`IceCreamId`, `Name`)
VALUES (@p2, @p3);
SELECT `IceCreamVariationId`
FROM `IceCreamVariations`
WHERE ROW_COUNT() = 1 AND `IceCreamVariationId` = LAST_INSERT_ID();
INSERT INTO `IceCreamVariations` (`IceCreamId`, `Name`)
VALUES (@p4, @p5);
SELECT `IceCreamVariationId`
FROM `IceCreamVariations`
WHERE ROW_COUNT() = 1 AND `IceCreamVariationId` = LAST_INSERT_ID();
INSERT INTO `IceCreamVariations` (`IceCreamId`, `Name`)
VALUES (@p6, @p7);
SELECT `IceCreamVariationId`
FROM `IceCreamVariations`
WHERE ROW_COUNT() = 1 AND `IceCreamVariationId` = LAST_INSERT_ID();
INSERT INTO `IceCreamVariations` (`IceCreamId`, `Name`)
VALUES (@p8, @p9);
SELECT `IceCreamVariationId`
FROM `IceCreamVariations`
WHERE ROW_COUNT() = 1 AND `IceCreamVariationId` = LAST_INSERT_ID();
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `i`.`IceCreamId`, `i`.`Name`, `i0`.`IceCreamVariationId`, `i0`.`IceCreamId`, `i0`.`Name`
FROM `IceCreams` AS `i`
LEFT JOIN `IceCreamVariations` AS `i0` ON `i`.`IceCreamId` = `i0`.`IceCreamId`
ORDER BY `i`.`IceCreamId`, `i0`.`IceCreamVariationId`
Мне интересно, если это поведение настраивается как-то и почему связанные сущности удаляются только после вставки, когда Viceversa кажется более безопасным.
Вы можете видеть, что операторы DELETE
выполняются до операторов INSERT
.
Обновление
С предоставленным вами новым примером кода я могу воспроизвести проблему. Это похоже на ошибку в EF Core (не Pomelo), потому что такое же поведение может быть воспроизведено с SQL Server:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public ICollection<IceCreamVariation> Variations { get; set; } = new HashSet<IceCreamVariation>();
}
public class IceCreamVariation
{
public int IceCreamVariationId { get; set; }
public string Name { get; set; }
public int UniqueId { get; set; }
public int IceCreamId { get; set; }
public IceCream IceCream { get; set; }
public ICollection<IceCreamVariationQuality> Qualities { get; set; } = new HashSet<IceCreamVariationQuality>();
}
public class IceCreamVariationQuality
{
public int IceCreamVariationQualityId { get; set; }
public string Name { get; set; }
public int IceCreamVariationId { get; set; }
public IceCreamVariation IceCreamVariation { get; set; }
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
public DbSet<IceCreamVariation> IceCreamVariations { get; set; }
public DbSet<IceCreamVariationQuality> IceCreamVariationQualities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(@"Data Source=.\MSSQL14;Integrated Security=true;Initial Catalog=So61383388_01")
//.UseMySql(
// "server=127.0.0.1;port=3308;user=root;password=;database=So61383388_01",
// b => b.ServerVersion("8.0.20-mysql"))
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>()
.HasData(
new IceCream {IceCreamId = 1, Name = "Vanilla"}
);
modelBuilder.Entity<IceCreamVariation>(
entity =>
{
entity.HasAlternateKey(e => e.UniqueId);
entity.HasData(
new IceCreamVariation
{
IceCreamVariationId = 1,
Name = "Double Vanilla Bourbon",
UniqueId = 42, // this value is part of a unique index
IceCreamId = 1
}
);
});
modelBuilder.Entity<IceCreamVariationQuality>()
.HasData(
new IceCreamVariationQuality
{
IceCreamVariationQualityId = 1,
Name = "Yummy",
IceCreamVariationId = 1
}
);
}
}
internal class Program
{
private static void Main()
{
using (var context = new Context())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var iceCreamWithOldVariations = context.IceCreams
.Include(i => i.Variations)
.ThenInclude(i => i.Qualities)
.OrderBy(i => i.IceCreamId)
.First();
Debug.Assert(iceCreamWithOldVariations.Variations.Count == 1);
Debug.Assert(iceCreamWithOldVariations.Variations.Single().UniqueId == 42);
Debug.Assert(iceCreamWithOldVariations.Variations.Single().Qualities.First().Name == "Yummy");
iceCreamWithOldVariations.Variations.Clear();
iceCreamWithOldVariations.Variations.Add(
new IceCreamVariation
{
Name = "Vanilla Cheesecake",
UniqueId = 42, // use same value again; should work because previous entity was removed
Qualities = new[]
{
new IceCreamVariationQuality { Name = "Healthy" },
},
});
context.SaveChanges();
var iceCreamWithNewVariations = context.IceCreams
.Include(i => i.Variations)
.ThenInclude(i => i.Qualities)
.OrderBy(i => i.IceCreamId)
.First();
Debug.Assert(iceCreamWithNewVariations.Variations.Count == 1);
Debug.Assert(iceCreamWithNewVariations.Variations.Single().UniqueId == 42);
Debug.Assert(iceCreamWithNewVariations.Variations.Single().Qualities.First().Name == "Healthy");
}
}
}
}
Если выполняется, когда объявлено свойство UniqueId
, исключение генерируется SQL еще более неожиданным:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [Parameters=[@p0='1'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [IceCreamVariationQualities]
WHERE [IceCreamVariationQualityId] = @p0;
SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[@p1='1', @p0='Vanilla Cheesecake' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [IceCreamVariations] SET [Name] = @p0
WHERE [IceCreamVariationId] = @p1;
SELECT [IceCreamVariationId]
FROM [IceCreamVariations]
WHERE @@ROWCOUNT = 1 AND [IceCreamVariationId] = scope_identity();
Здесь сгенерирован оператор UPDATE
для изменения имени IceCreamVariations
сущности (что неверно), что предполагает, что объект был только что вставлен (что не так), потому что он использует scope_identity()
.
Обходной путь:
Обсуждение продолжено GitHub .
Там @smitpatel предложил попробовать entity.HasIndex(e => e.UniqueId).IsUnique()
вместо entity.HasAlternateKey(e => e.UniqueId)
, что работает, как и ожидалось.
Таким образом, определяя уникальный индекс вместо альтернативного ключа в определении модели допустимый обходной путь для этой проблемы:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ...
modelBuilder.Entity<IceCreamVariation>(
entity =>
{
// This does not work:
// entity.HasAlternateKey(e => e.UniqueId);
// This *does* work:
entity.HasIndex(e => e.UniqueId)
.IsUnique();
entity.HasData(
new IceCreamVariation
{
IceCreamVariationId = 1,
Name = "Double Vanilla Bourbon",
UniqueId = 42, // this value is part of a unique index
IceCreamId = 1
}
);
});
// ...
}