-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Topological sort should consider unique constraints for ordering #20870
Comments
What kind of constraint your table have? |
@smitpatel The issue can be reproduced with SQL Server as well: 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=3306;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");
}
}
}
} If executed when the 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(); Here, an This is only an issue, when deleting grand child and child entities (hierarchy must be two levels deep). It works as expected for one level deep deletes. This issue discussion started at StackOverflow.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (8ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [IceCreams] (
[IceCreamId] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
CONSTRAINT [PK_IceCreams] PRIMARY KEY ([IceCreamId])
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [IceCreamVariations] (
[IceCreamVariationId] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[UniqueId] int NOT NULL, /* <-- uses UNIQUE constraint/index */
[IceCreamId] int NOT NULL,
CONSTRAINT [PK_IceCreamVariations] PRIMARY KEY ([IceCreamVariationId]),
CONSTRAINT [AK_IceCreamVariations_UniqueId] UNIQUE ([UniqueId]),
CONSTRAINT [FK_IceCreamVariations_IceCreams_IceCreamId] FOREIGN KEY ([IceCreamId]) REFERENCES [IceCreams] ([IceCreamId]) ON DELETE CASCADE
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [IceCreamVariationQualities] (
[IceCreamVariationQualityId] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[IceCreamVariationId] int NOT NULL,
CONSTRAINT [PK_IceCreamVariationQualities] PRIMARY KEY ([IceCreamVariationQualityId]),
CONSTRAINT [FK_IceCreamVariationQualities_IceCreamVariations_IceCreamVariationId] FOREIGN KEY ([IceCreamVariationId]) REFERENCES [IceCreamVariations] ([IceCreamVariatio
nId]) ON DELETE CASCADE
);
/* ... */
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE INDEX [IX_IceCreamVariationQualities_IceCreamVariationId] ON [IceCreamVariationQualities] ([IceCreamVariationId]);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE INDEX [IX_IceCreamVariations_IceCreamId] ON [IceCreamVariations] ([IceCreamId]); So the child table The grand child table |
Does it work if you configure unique index in EF core rather than alternate key? |
Yes, using an unique index instead of an alternate key does work as expected! (/cc @shoooe) info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (18ms) [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'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [IceCreamVariations]
WHERE [IceCreamVariationId] = @p1;
SELECT @@ROWCOUNT;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[@p2='1', @p3='Vanilla Cheesecake' (Size = 4000), @p4='42'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [IceCreamVariations] ([IceCreamId], [Name], [UniqueId])
VALUES (@p2, @p3, @p4);
SELECT [IceCreamVariationId]
FROM [IceCreamVariations]
WHERE @@ROWCOUNT = 1 AND [IceCreamVariationId] = scope_identity();
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[@p5='2', @p6='Healthy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [IceCreamVariationQualities] ([IceCreamVariationId], [Name])
VALUES (@p5, @p6);
SELECT [IceCreamVariationQualityId]
FROM [IceCreamVariationQualities]
WHERE @@ROWCOUNT = 1 AND [IceCreamVariationQualityId] = scope_identity(); Interesting though, that without the unique constraint (when we remove I would have expected a dependency walker to figure out the dependency order, and then first to execute all deletes (in dependency order), then all inserts (in dependency order) and finally all updates (in dependency order). But I am probably missing something here. |
My specific constraint is a two column index, I'll give that a try and update the issue. |
Yes, I can confirm that specifying the index solves the issue. |
No, this should not be closed, because alternate keys should generally also work in the described case, but don't at the moment. |
Don't use entry sharing for entities with the same AK values Only remove the entry with duplicate key value from the identity map if it's still there Fixes #20870
Don't use entry sharing for entities with the same AK values Only remove the entry with duplicate key value from the identity map if it's still there Fixes #20870
Don't use entry sharing for entities with the same AK values Only remove the entry with duplicate key value from the identity map if it's still there Fixes #20870
Given these tables:
And these relationships:
If I try to replace the list of variations of an IceCream I see the following behavior:
IceCreamVariationQuality
s are removed firstIceCreamVariation
s are addedIceCreamVariation
s are removedNow #1 is fine. But the ordering of #2 and #3 causes problems with regards for example to unique constraints. I would have expected #3 to happen before #2.
It's true both orderings could cause issues because it depends on the kind of constraints the table has, but I would guess going #3 then #2 would generally be better.
Steps to reproduce
The example below uses Pomelo (MySQL) driver but I've been told the same can be reproduced in SQL Server:
The code above yields the following log:
Further technical details
EF Core version: 3.1.1
Database provider: Pomelo.EntityFrameworkCore.MySql 3.1.1
Target framework: .NET Core 3.1
Operating system: macOS Mojave
IDE: -
The text was updated successfully, but these errors were encountered: