• Entity Framework Core 关联删除


    关联删除通常是一个数据库术语,用于描述在删除行时允许自动触发删除关联行的特征;即当主表的数据行被删除时,自动将关联表中依赖的数据行进行删除,或者将外键更新为NULL或默认值。

    数据库关联删除行为

    我们先来看一看SQL Server中支持的行为。在创建外键约束时,可以指定关联表在主表删除行时,对依赖的数据如何执行操作。例如下面的SQL语句,[Order Details]表中[OrderID]字段 是外键,依赖于[Orders]表中的主键[OrderID]

    CREATE TABLE [Orders] (
        [OrderID] int NOT NULL IDENTITY,
        [Name] nvarchar(max) NULL,
        [OrderDate] datetime2 NULL,
        CONSTRAINT [PK_Orders] PRIMARY KEY ([OrderID])
    );
    
    GO
    
    CREATE TABLE [Order Details] (
        [DetailId] int NOT NULL IDENTITY,
        [OrderID] int NULL,
        [ProductID] int NOT NULL,
        CONSTRAINT [PK_Order Details] PRIMARY KEY ([DetailId]),
        CONSTRAINT [FK_Order Details_Orders_OrderID] FOREIGN KEY ([OrderID]) REFERENCES [Orders] ([OrderID]) ON DELETE SET NULL
    );
    
    

    外键约束[FK_Order Details_Orders_OrderID]末尾的语句是ON DELETE SET NULL,表示当主表的数据行删除时,自动将关联表数据行的外键更新为NULL

    在SQL Server中支持如下四种行为:

    • ON DELETE NO ACTION
      默认行为,删除主表数据行时,依赖表中的数据不会执行任何操作,此时会产生错误,并回滚DELETE语句。例如会产生下面的错误:

      DELETE 语句与 REFERENCE 约束"FK_Order Details_Orders_OrderID"冲突。该冲突发生于数据库"Northwind_Test",表"dbo.Order Details", column 'OrderID'。
      语句已终止。

    • ON DELETE CASCADE
      删除主表数据行时,依赖表的中数据行也会同步删除。
    • ON DELETE SET NULL
      删除主表数据行时,将依赖表中数据行的外键更新为NULL。为了满足此约束,目标表的外键列必须可为空值。
    • ON DELETE SET DEFAULT
      删除主表数据行时,将依赖表的中数据行的外键更新为默认值。为了满足此约束,目标表的所有外键列必须具有默认值定义;如果外键可为空值,并且未显式设置默认值,则将使用NULL作为该列的隐式默认值。

    简单介绍了数据库中行为后,我们来着重介绍 EF Core 中的关联实体的行为。

    定义实体

    我们先定义两个实体OrderOrderDetail分别表示订单和订单明细;其中OrderOrderDetail的关系是一对多,在OrderDetail实体中OrderID表示外键,依赖于Order实体中的主键OrderID

        public class Order
        {
            public int OrderID { get; set; }
    
            public string Name { get; set; }
    
            public DateTime? OrderDate { get; set; }
    
            public ICollection<OrderDetail> OrderDetails { get; set; }
        }
    
        public class OrderDetail
        {
            public int DetailId { get; set; }
    
            public int? OrderID { get; set; }
            
            public int ProductID { get; set; }
    
            public Order Order { get; set; }
        }
    
    

    Fluent API 配置关联实体

    DbContextOnModelCreating方法中,我们使用 Fluent API 配置实体中之间的关系。

        public class NorthwindContext : DbContext
        {
    
            public virtual DbSet<Order> Orders { get; set; }
            public virtual DbSet<OrderDetail> OrderDetails { get; set; }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Order>(
                    builder =>
                    {
                        builder.HasMany<OrderDetail>(e => e.OrderDetails).WithOne(e => e.Order).HasForeignKey(e => e.OrderID).OnDelete(DeleteBehavior.ClientSetNull);
                    });
            }
        }
    
    

    OnDelete方法中,需要传递参数DeleteBehavior枚举,分别有如下四个值:

        public enum DeleteBehavior
        {
            Cascade,
    
            SetNull,
    
            ClientSetNull,
    
            Restrict
        }    
    

    这四个枚举值的分别表示不同的行为,这也是我们今天的重点。

    创建表结构

    我们分别使用使用这这个枚举值,来创建数据表结构。

    
            [InlineData(DeleteBehavior.Cascade)]
            [InlineData(DeleteBehavior.SetNull)] 
            [InlineData(DeleteBehavior.ClientSetNull)]
            [InlineData(DeleteBehavior.Restrict)]
            [Theory]
            public void Create_Database(DeleteBehavior behavior)
            {
                using (var northwindContext = new NorthwindContext(behavior))
                {
                    northwindContext.Database.EnsureDeleted();
                    northwindContext.Database.EnsureCreated();
                }
            }
            
    

    四个枚举值创建表的SQL语句类似如下,唯一区别在于创建外键约束[FK_Order Details_Orders_OrderID]ON DELETE {} 后面的语句。

    
    CREATE TABLE [Orders] (
        [OrderID] int NOT NULL IDENTITY,
        [Name] nvarchar(max) NULL,
        [OrderDate] datetime2 NULL,
        CONSTRAINT [PK_Orders] PRIMARY KEY ([OrderID])
    );
    
    GO
    
    CREATE TABLE [Order Details] (
        [DetailId] int NOT NULL IDENTITY,
        [OrderID] int NOT NULL,
        [ProductID] int NOT NULL,
        CONSTRAINT [PK_Order Details] PRIMARY KEY ([DetailId]),
        CONSTRAINT [FK_Order Details_Orders_OrderID] FOREIGN KEY ([OrderID]) REFERENCES [Orders] ([OrderID]) ON DELETE CASCADE
    );
    
    

    四个枚举值分别对应的SQL语句如下:

    枚举值 SQL语句
    DeleteBehavior.Cascade ON DELETE CASCADE
    DeleteBehavior.SetNull ON DELETE SET NULL
    DeleteBehavior.ClientSetNull ON DELETE NO ACTION
    DeleteBehavior.Restrict ON DELETE NO ACTION

    EF Core 关联实体删除行为

    我们分别通过枚举值与是否跟踪关联实体,进行代码测试,测试代码如下:

    
            [InlineData(DeleteBehavior.Cascade, true)]
            [InlineData(DeleteBehavior.Cascade, false)]
            [InlineData(DeleteBehavior.SetNull, true)]
            [InlineData(DeleteBehavior.SetNull, false)]
            [InlineData(DeleteBehavior.ClientSetNull, true)]
            [InlineData(DeleteBehavior.ClientSetNull, false)]
            [InlineData(DeleteBehavior.Restrict, true)]
            [InlineData(DeleteBehavior.Restrict, false)]
    
            [Theory]
            public void Execute(DeleteBehavior behavior, bool includeDetail)
            {
                using (var northwindContext = new NorthwindContext(behavior))
                {
                    northwindContext.Database.EnsureDeleted();
                    northwindContext.Database.EnsureCreated();
                }
    
                int orderId;
                int detailId;
                using (var northwindContext = new NorthwindContext(behavior))
                {
                    var order = new Order {
                        Name = "Order1"
                    };
    
                    var orderDetail = new OrderDetail {
                        ProductID = 11
                    };
                    order.OrderDetails = new List<OrderDetail> {
                        orderDetail
                    };
    
    
                    northwindContext.Set<Order>().Add(order);
                                    northwindContext.SaveChanges();
    
                    orderId = order.OrderID;
                    detailId = orderDetail.DetailId;
                }
    
                using (var northwindContext = new NorthwindContext(behavior))
                {
                    var queryable = northwindContext.Set<Order>().Where(e => e.OrderID == orderId);
                    if (includeDetail){
                        queryable = queryable.Include(e => e.OrderDetails);
                    }
    
                    var order = queryable.Single(); 
                    northwindContext.Set<Order>().Remove(order);
    
                    try
                    {
                        northwindContext.SaveChanges();
                        DumpSql();
                    }
                    catch (Exception)
                    {
                        DumpSql();
                        throw;
                    }
    
                }
    
                using (var northwindContext = new NorthwindContext(behavior))
                {
                    var orderDetail = northwindContext.Set<OrderDetail>().Find(detailId);
                    if (behavior == DeleteBehavior.Cascade)
                    {
                        Assert.Null(orderDetail);
                    }
                    else
                    {
                        Assert.NotNull(orderDetail);
                    }
                }
            }
            
    
    枚举值 是否跟踪关联实体 是否成功调用SaveChange 关联实体是否存在 执行的SQL
    DeleteBehavior.Cascade No 成功 DELETE FROM [Orders] WHERE [OrderID] = 1
    DeleteBehavior.Cascade YES 成功 DELETE FROM [Order Details] WHERE [DetailId] = 1
    DELETE FROM [Orders] WHERE [OrderID] = 1
    DeleteBehavior.SetNull No 成功 YES
    (外键为NULL
    DELETE FROM [Orders] WHERE [OrderID] = 1
    DeleteBehavior.SetNull YES 成功 YES
    (外键为NULL
    UPDATE [Order Details] SET [OrderID] = NULL WHERE [DetailId] = 1
    DELETE FROM [Orders] WHERE [OrderID] = 1
    DeleteBehavior.ClientSetNull No 失败
    (外键约束)
    YES DELETE FROM [Orders] WHERE [OrderID] = 1
    DeleteBehavior.ClientSetNull YES 成功 YES
    (外键为NULL
    UPDATE [Order Details] SET [OrderID] = NULL WHERE [DetailId] = 1
    DELETE FROM [Orders] WHERE [OrderID] = 1
    DeleteBehavior.Restrict No 失败
    (外键约束)
    YES DELETE FROM [Orders] WHERE [OrderID] = 1
    DeleteBehavior.Restrict YES 失败
    (外键约束)
    YES DELETE FROM [Orders] WHERE [OrderID] = 1

    总结

    根据上面的测试结果,我们可以出得如下结论:

    DeleteBehavior.Cascade

    • 如果关联实体未被跟踪,主实体的状态标记为删除,执行SaveChage时,在删除主表的数据的同时,通过数据库的行为删除关联表的数据行;
    • 如果关联实体已经被跟踪,将主实体的状态标记为删除时,关联实体的状态也会标记为删除,执行SaveChange时,先删除关联表的数据行,然后再删除主表的数据行;
    • 外键可以设置非空值、也可以设置为可为空值;
    • 关联实体可以不被跟踪。

    DeleteBehavior.SetNull

    • 如果关联实体未被跟踪,主实体的状态标记为删除,执行SaveChage时,在删除主表的数据时,通过数据库的行为将关联表数据行的外键更新为NULL,;
    • 如果关联实体已经被跟踪,将主实体的状态标记为删除时,关联实体的外键会被设置为null,同时将关联实体的状态标记为修改,执行SaveChange时,先更新关联表的数据行 ,然后删除主表的数据行;
    • 因为要将外键更新为NULL,所以外键必须设置为可空字段;
    • 关联实体可以不被跟踪。

    DeleteBehavior.ClientSetNull

    • 数据库不会执行任何行为;
    • 关联实体必须被跟踪,将主实体的状态标记为删除时,关联实体的外键被设置为null,同时将关联实体的状态标记为修改,执行SaveChange时,先更新关联表的数据行,然后删除主表的数据行(此时的行为与DeleteBehavior.SetNull一致);
    • 因为要将外键更新为NULL,所以外键必须设置为可空字段;
    • 关联实体必须被跟踪,否则保存数据时会抛出异常。

    DeleteBehavior.Restrict

    • 框架不执行任何操作,由开发人员决定关联实体的行为,可以将关联实体的状态设置为删除,也可以将关联实体的外键设置为null
    • 因为要修改关联实体的状态或外键的值,所以关联实体必须被跟踪。
  • 相关阅读:
    python_控制台输出带颜色的文字方法
    模拟数据库作业
    js笔记
    CSS 笔记
    html 笔记
    必备技能-Git 使用规范流程
    python 闭包
    30个python编程技巧!
    python 面向对象
    python 线程
  • 原文地址:https://www.cnblogs.com/tdfblog/p/entity-framework-core-cascade-delete.html
Copyright © 2020-2023  润新知