为什么Entity Framework不能处理可能是子项的级联删除?

huangapple go评论54阅读模式
英文:

Why can't Entity Framework handle cascade deletes on a maybe child

问题

以下是您提供的代码部分的中文翻译:

这是一个政治竞选系统。在其中,我们有竞选模型。竞选可以是国家级(总统)或地方级。地方竞选由相应的州份拥有。因此,一个竞选活动只有一个父级,但它可以是一个国家或一个州。

public class Campaign : IOrganization
{
    public int Id { get; private set; }

    public State? State { get; set; }
    public int? StateId { get; set; }

    public Country? Country { get; set; }
    public int? CountryId { get; set; }
}

州和国家各自拥有他们所拥有的竞选活动的集合。

public class Country : IOrganization
{
    public int Id { get; private set; }

    public ICollection<Campaign>? Campaigns { get; private set; }
}

public class State : IOrganization
{
    public int Id { get; private set; }

    public ICollection<Campaign>? Campaigns { get; private set; }
}

当我运行 update-database 时,出现以下错误:

Microsoft.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint 'FK_Campaigns_States_StateId' on table 'Campaigns' 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.

我可以通过以下的 Fluent API 避免这个问题:

// 对于国家也是一样
public void Configure(EntityTypeBuilder<State> builder)
{
    builder.HasMany(x => x.Campaigns)
           .WithOne(x => x.State).OnDelete(DeleteBehavior.ClientSetNull);
}

然后,我在我的 DbContext.SaveChangesAsync() 重写中找出需要删除的竞选活动并在那里删除它们。

是否有一种方式告诉 EF 不要担心,它可以级联删除,不会出现 Campaign 同时有 StateCountry 两个父级的情况?因为我认为让数据库强制执行级联删除会更好。

更新: 我刚刚在 MSDN 上发布了这个问题,以查看他们是否能够回答。

希望这对您有所帮助。

英文:

This is for a political campaign system. In it we have the campaign model. The campaign can be national (President) or local. A local campaign is owned by the appropriate State. So a Campaign has exactly 1 parent, but it can be a Country or State.

public class Campaign : IOrganization
{
	public int Id { get; private set; }

	public State? State { get; set; }
	public int? StateId { get; set; }

	public Country? Country { get; set; }
	public int? CountryId { get; set; }
}

And State & Country have their collections of the Campaigns they each own.

public class Country : IOrganization
{
	public int Id { get; private set; }

	public ICollection&lt;Campaign&gt;? Campaigns { get; private set; }
}

public class State : IOrganization
{
    public int Id { get; private set; }

	public ICollection&lt;Campaign&gt;? Campaigns { get; private set; }
}

When I run update-database I get:

fail: Microsoft.EntityFrameworkCore.Database.Command[20102]
      Failed executing DbCommand (9ms) [Parameters=[], CommandType=&#39;Text&#39;, CommandTimeout=&#39;30&#39;]
      CREATE TABLE [Campaigns] (
          [Id] int NOT NULL IDENTITY,
          [UniqueId] nvarchar(450) NOT NULL,
          [Name] nvarchar(450) NOT NULL,
          [News] nvarchar(max) NULL,
          [Address_Address1] nvarchar(max) NULL,
          [Address_Address2] nvarchar(max) NULL,
          [Address_City] nvarchar(max) NULL,
          [Address_State] nvarchar(max) NULL,
          [Address_Country] nvarchar(max) NULL,
          [Address_PostalCode] nvarchar(max) NULL,
          [Address_Location] geography NULL,
          [StateId] int NULL,
          [CountryId] int NULL,
          [Enabled] bit NOT NULL,
          [Closed] bit NOT NULL,
          [Deleted] bit NOT NULL,
          [Created] datetime2 NOT NULL DEFAULT (getutcdate()),
          [RowVersion] rowversion NOT NULL,
          CONSTRAINT [PK_Campaigns] PRIMARY KEY ([Id]),
          CONSTRAINT [FK_Campaigns_Countries_CountryId] FOREIGN KEY ([CountryId]) REFERENCES [Countries] ([Id]) ON DELETE CASCADE,
          CONSTRAINT [FK_Campaigns_States_StateId] FOREIGN KEY ([StateId]) REFERENCES [States] ([Id]) ON DELETE CASCADE
      );
Failed executing DbCommand (9ms) [Parameters=[], CommandType=&#39;Text&#39;, CommandTimeout=&#39;30&#39;]
CREATE TABLE [Campaigns] (
    [Id] int NOT NULL IDENTITY,
    [UniqueId] nvarchar(450) NOT NULL,
    [Name] nvarchar(450) NOT NULL,
    [News] nvarchar(max) NULL,
    [Address_Address1] nvarchar(max) NULL,
    [Address_Address2] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_State] nvarchar(max) NULL,
    [Address_Country] nvarchar(max) NULL,
    [Address_PostalCode] nvarchar(max) NULL,
    [Address_Location] geography NULL,
    [StateId] int NULL,
    [CountryId] int NULL,
    [Enabled] bit NOT NULL,
    [Closed] bit NOT NULL,
    [Deleted] bit NOT NULL,
    [Created] datetime2 NOT NULL DEFAULT (getutcdate()),
    [RowVersion] rowversion NOT NULL,
    CONSTRAINT [PK_Campaigns] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Campaigns_Countries_CountryId] FOREIGN KEY ([CountryId]) REFERENCES [Countries] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_Campaigns_States_StateId] FOREIGN KEY ([StateId]) REFERENCES [States] ([Id]) ON DELETE CASCADE
);
Microsoft.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint &#39;FK_Campaigns_States_StateId&#39; on table &#39;Campaigns&#39; 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&amp; 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&amp; 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.Migrations.Internal.Migrator.Migrate(String targetMigration)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.UpdateDatabase(String targetMigration, String connectionString, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabaseImpl(String targetMigration, String connectionString, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabase.&lt;&gt;c__DisplayClass0_0.&lt;.ctor&gt;b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
ClientConnectionId:e68c158d-e2e8-4149-9d65-7f6fa23bf360
Error Number:1785,State:0,Class:16
Introducing FOREIGN KEY constraint &#39;FK_Campaigns_States_StateId&#39; on table &#39;Campaigns&#39; 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.

I can avoid this with the following fluent:

// one for Country too
public void Configure(EntityTypeBuilder&lt;State&gt; builder)
{
	builder.HasMany(x =&gt; x.Campaigns)
			.WithOne(x =&gt; x.State).OnDelete(DeleteBehavior.ClientSetNull);
}

And I then in my DbContext.SaveChangesAsync() override figure out what campaigns need to be deleted and delete them there.

Is there some way to tell EF to not worry, that it can cascade delete and there will not be any cases where a Campaign has both a State and a Country parent? Because I figure it's much better to have the DB enforce a cascading delete.

Update: I posted it on MSDN just now to see if they can answer.

答案1

得分: 1

以下是翻译好的部分:

以下的代码有效,并且通过了我所有的单元测试。但是如果/当了解 EF 的人评论并确认这是正确的解决方案时,我会感到更放心。(或者发布一个回答说这是错误的,并提供正确的答案。)

public class StateConfiguration : IEntityTypeConfiguration<State>
{
    public void Configure(EntityTypeBuilder<State> builder)
    {
        builder.HasMany(x => x.Campaigns)
            .WithOne(x => x.State).OnDelete(DeleteBehavior.ClientCascade);
    }
}

public class CountryConfiguration : IEntityTypeConfiguration<Country>
{
    public void Configure(EntityTypeBuilder<Country> builder)
    {
        builder.HasMany(x => x.Campaigns)
            .WithOne(x => x.Country).OnDelete(DeleteBehavior.ClientCascade);
    }
}

阅读了一些网站后,我认为 `ClientCascade` 的区别是 EF 执行级联删除。让数据库来执行可能更有效,但让 EF 来执行可能比我自己执行更有效。所以我会采纳它。
英文:

The following works and passes all my unit tests. But I'll feel better about this solution if/when someone who knows EF comments on this and says this is right. (Or posts an answer saying this is wrong and providing the correct answer.)

public class StateConfiguration : IEntityTypeConfiguration&lt;State&gt;
{
	public void Configure(EntityTypeBuilder&lt;State&gt; builder)
	{
		builder.HasMany(x =&gt; x.Campaigns)
			.WithOne(x =&gt; x.State).OnDelete(DeleteBehavior.ClientCascade);
	}
}

public class CountryConfiguration : IEntityTypeConfiguration&lt;Country&gt;
{
	public void Configure(EntityTypeBuilder&lt;Country&gt; builder)
	{
		builder.HasMany(x =&gt; x.Campaigns)
			.WithOne(x =&gt; x.Country).OnDelete(DeleteBehavior.ClientCascade);
	}
}

From reading a couple of sites I think the difference is ClientCascade has EF perform the cascading delete. Having the DB do it is likely more efficient, but having EF do it is likely more efficient than doing it myself. So I'll take it.

答案2

得分: -1

错误已在此处记录。

为什么Entity Framework不能处理可能是子项的级联删除?

但奇怪的是,您会遇到此错误,考虑到每个目标表最多只有一个外键。所以,也许这是 SQL Server 中的某个 bug。解决的另一种方法是创建一个在删除之前触发器。示例:

DELIMITER $$

CREATE TRIGGER trigger_name
    BEFORE DELETE
    ON table_name FOR EACH ROW
BEGIN
    -- 语句
END$$    

DELIMITER ;

让我们将这个思路应用到您的表上:

DELIMITER $$

CREATE TRIGGER Delete_Country
    BEFORE DELETE
    ON Countries FOR EACH ROW
BEGIN
    DELETE FROM campaigns where CountryId = OLD.Id;
END$$    

CREATE TRIGGER Delete_State
    BEFORE DELETE
    ON States FOR EACH ROW
BEGIN
    DELETE FROM campaigns where StateId = OLD.Id;
END$$    

DELIMITER ;
英文:

The error was documented here.

为什么Entity Framework不能处理可能是子项的级联删除?

But it's strange that you get this error, given the fact that you have a single foreign key at most per target tables. So, maybe it's some bug in SQL Server. An alternate way to solve it is to create a before delete trigger. Sample example:

DELIMITER $$

CREATE TRIGGER trigger_name
    BEFORE DELETE
    ON table_name FOR EACH ROW
BEGIN
    -- statements
END$$    

DELIMITER ;

Let's apply the idea to your table:

DELIMITER $$

CREATE TRIGGER Delete_Country
    BEFORE DELETE
    ON Countries FOR EACH ROW
BEGIN
    DELETE FROM campaigns where CountryId = OLD.Id;
END$$    

CREATE TRIGGER Delete_State
    BEFORE DELETE
    ON States FOR EACH ROW
BEGIN
    DELETE FROM campaigns where StateId = OLD.Id;
END$$    

DELIMITER ;

huangapple
  • 本文由 发表于 2023年6月6日 05:03:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76409995.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定