efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Self-referencing many-to-many between TPH subtypes creates cascade cycles

Open ajcvickers opened this issue 3 years ago • 0 comments

These types create a valid model for SQL Server:

public abstract class Animal
{
    public int Id { get; set; }

    public ICollection<Animal> Humans { get; } = new List<Animal>();
    public ICollection<Animal> Pets { get; } = new List<Animal>();
}

public class Pet : Animal
{
}

public class Human : Animal
{
}

Notice that only one direction is configured as Cascade.

Model:
  EntityType: Animal Abstract
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Discriminator (no field, string) Shadow Required AfterSave:Throw
    Skip navigations:
      Humans (ICollection<Animal>) CollectionAnimal Inverse: Pets
      Pets (ICollection<Animal>) CollectionAnimal Inverse: Humans
    Keys:
      Id PK
  EntityType: AnimalAnimal (Dictionary<string, object>) CLR Type: Dictionary<string, object>
    Properties:
      HumansId (no field, int) Indexer Required PK FK AfterSave:Throw
      PetsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      HumansId, PetsId PK
    Foreign keys:
      AnimalAnimal (Dictionary<string, object>) {'HumansId'} -> Animal {'Id'} Cascade
      AnimalAnimal (Dictionary<string, object>) {'PetsId'} -> Animal {'Id'} ClientCascade
    Indexes:
      PetsId
  EntityType: Human Base: Animal
  EntityType: Pet Base: Animal

But moving the relationships to subtypes:

public abstract class Animal
{
    public int Id { get; set; }
}

public class Pet : Animal
{
    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class Human : Animal
{
    public ICollection<Pet> Pets { get; } = new List<Pet>();
}

Results in both types being Cascade.

Model:
  EntityType: Animal Abstract
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Discriminator (no field, string) Shadow Required AfterSave:Throw
    Keys:
      Id PK
  EntityType: Human Base: Animal
    Skip navigations:
      Pets (ICollection<Pet>) CollectionPet Inverse: Humans
  EntityType: HumanPet (Dictionary<string, object>) CLR Type: Dictionary<string, object>
    Properties:
      HumansId (no field, int) Indexer Required PK FK AfterSave:Throw
      PetsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      HumansId, PetsId PK
    Foreign keys:
      HumanPet (Dictionary<string, object>) {'HumansId'} -> Human {'Id'} Cascade
      HumanPet (Dictionary<string, object>) {'PetsId'} -> Pet {'Id'} Cascade
    Indexes:
      PetsId
  EntityType: Pet Base: Animal
    Skip navigations:
      Humans (ICollection<Human>) CollectionHuman Inverse: Pets

Which throws on SQL Server:

Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint 'FK_HumanPet_Animals_PetsId' on table 'HumanPet' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean isAsync, Int32 timeout, Boolean asyncWrite)
   at Microsoft.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String methodName)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteNonQuery(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Migrations.MigrationCommand.ExecuteNonQuery(IRelationalConnection connection, IReadOnlyDictionary`2 parameterValues)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationCommandExecutor.ExecuteNonQuery(IEnumerable`1 migrationCommands, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseCreator.CreateTables()
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseCreator.EnsureCreated()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureCreated()
   at Program.Main() in C:\local\code\AllTogetherNow\Daily\Daily.cs:line 80
ClientConnectionId:a095e670-5eed-42ba-bdef-57d7e30a170d
Error Number:1785,State:0,Class:16

Full code:

public static class Your
{
    public static string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Database=Test";
}

public abstract class Animal
{
    public int Id { get; set; }
}

public class Pet : Animal
{
    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class Human : Animal
{
    public ICollection<Pet> Pets { get; } = new List<Pet>();
}

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            //.UseNpgsql("Server=127.0.0.1;Port=5432;Database=myDataBase;User Id=myUsername;Password=myPassword;")
            //.UseSqlite("Data Source=test.db")
            //.UseInMemoryDatabase("Test")
            // .UseLazyLoadingProxies()
            .UseSqlServer(Your.ConnectionString)
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    public DbSet<Animal> Animals => Set<Animal>();
    public DbSet<Human> Humans => Set<Human>();
    public DbSet<Pet> Pets => Set<Pet>();
}

public class Program
{
    public static void Main()
    {
        using (var context = new SomeDbContext())
        {
            Console.WriteLine(context.Model.ToDebugString());
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();
        }
    }
}

ajcvickers avatar Aug 09 '22 16:08 ajcvickers