Я использую Entity Framework Core 3 и использую провайдера Pomelo MySQL. Я пытаюсь обновить дочернее свойство с помощью:
model.Items = collection;
где model
— объект, который существует в базе данных, а collection
— это ICollection<Item>
новых объектов, которые необходимо установить для этого списка.
Когда я запускаю:
dbContext.Models.Update(model);
await dbContext.SaveChangesAsync(ct);
Я заметил странное поведение (просматривая журналы запросов EF Core): ранее существующие элементы, связанные с моделью, удаляются не сразу, а только после вставки новых элементов в collection
.
Это вызывает ошибку ограничения целостности в базе данных, потому что на короткое время (между вставкой и последующим удалением) для некоторых уникальных столбцов появляются повторяющиеся значения.
Мне интересно, можно ли как-то настроить это поведение и почему связанные объекты удаляются только после вставки, когда наоборот кажется более безопасным.
Как воспроизвести
Чтобы воспроизвести проблему, вы можете использовать этот файл docker-compose.yml
для настройки базы данных MySQL 8:
version: '3.4'
services:
database:
image: mysql:8.0
environment:
MYSQL_USER: So61383388
MYSQL_PASSWORD: root
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: So61383388
ports:
- "3306:3306"
- "33060:33060"
а затем запустите эту программу:
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 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; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMySql(
"server=127.0.0.1;port=3306;user=root;password=root;database=So61383388",
b => b.ServerVersion("8.0"))
.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>()
.HasData(
new IceCreamVariation {IceCreamVariationId = 1, Name = "Double Vanilla Bourbon", IceCreamId = 1},
new IceCreamVariation {IceCreamVariationId = 2, Name = "Vanilla Caramel", IceCreamId = 1}
);
modelBuilder.Entity<IceCreamVariationQuality>()
.HasData(
new IceCreamVariationQuality {IceCreamVariationQualityId = 1, Name = "Fresh", IceCreamVariationId = 1},
new IceCreamVariationQuality {IceCreamVariationQualityId = 2, Name = "Yummy", IceCreamVariationId = 1},
new IceCreamVariationQuality {IceCreamVariationQualityId = 3, Name = "Woops", IceCreamVariationId = 2}
);
}
}
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)
.FirstOrDefault();
Debug.Assert(iceCreamWithOldVariations.Variations.Count == 2);
var vanillaIceCream = iceCreamWithOldVariations;
vanillaIceCream.Variations.Clear();
vanillaIceCream.Variations.Add(
new IceCreamVariation
{
Name = "Vanilla Cheesecake",
Qualities = new IceCreamVariationQuality[]
{
new IceCreamVariationQuality { Name = "Healthy" },
},
});
vanillaIceCream.Variations.Add(
new IceCreamVariation
{
Name = "Vanilla Cheesecake",
Qualities = new IceCreamVariationQuality[]
{
new IceCreamVariationQuality { Name = "Fresh" },
new IceCreamVariationQuality { Name = "Cool" },
},
});
context.SaveChanges();
var iceCreamWithNewVariations = context.IceCreams
.Include(i => i.Variations)
.ThenInclude(i => i.Qualities)
.OrderBy(i => i.IceCreamId)
.FirstOrDefault();
Debug.Assert(iceCreamWithNewVariations.Variations.Count == 2);
}
}
}
}
с этой консолью .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.1" />
</ItemGroup>
</Project>
с использованием:
docker-compose down
docker-compose up -d
dotnet run
Если вы видите журналы:
- сначала удаляются качества (что нормально)
- затем добавляются новые вариации (что неверно)
- старые варианты удаляются (что должно произойти до добавления новых вариантов)
Такое поведение не происходит, если у вас нет вложенных качеств для каждого варианта.