jk's notes
  • 删除数据

删除数据

本章中, 我会介绍使用 EFCore 5 如何删除数据. 有两种常用的方法去删除记录. 这两个方法被称为软删除和硬删除. 软删除是将记录标记为删除, 可以通过数据库更新, 设置标记记录为删除来实现. 而硬删除是将数据从数据库中删除掉, 并且之后再也无法查询出该数据. 本章, 会介绍如何实现硬删除.

删除根实体

要删除有两个步骤. 首先查询出需要删除的记录, 第二部使用 DbSet<T> 的 Remove 方法, 如下:

var existing = _context.Persons
    .Single(x => x.FirstName == "Clarke" && x.LastName == "Kent");
_context.Persons.Remove(existing);
_context.SaveChanges();

如你所看到的, 删除根实体很简单.

删除子实体

默认情况下, EF Core 5 在删除根实体的时候会附带将子实体一并删除. 在我们的数据库中, 如果你删除了 person 记录, 与之相关联的 Address 记录也会被删除. 你可以在 AppDbContext 类的 OnModelCreating 方法中配置来按照你的意愿来处理子实体如何删除. 你可以使用的选项有级联 (cascade), 客户端设置null(client set null), 约束(restrict), 以及设置 null(set null). 我会一步一步的说明每一个选项是如何工作的.

级联删除 (Cascade Delete)

级联删除是删除子实体的默认设置. 级联的含义是, 在删除父实体的时候, 所有相关联的子实体会全部删除. 换句话说, 当 person 被删除时, 它的地址记录也会被删除. 要显式的定义该行为, 我们需要完成 Address 和 Person 的一对多的映射. 我们通过在 Address 实体中添加名为 Person 的 Person 类型的属性来实现, 如下:

namespace EFCore5WebApp.Core.Entiies
{
    public class Address
    {
        public int Id { get; set; }
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Country { get; set; }
        public string ZipCode { get; set; }
        public int PersonId { get; set; }
        public Person Person { get; set; }
    }
}

你还可以在 OnModelCreating 事件中显式的定义该行为, 例如

modelBuilder.Entity<Person>(entity => 
{
    entity.HasMany(x => x.Addresses)
        .WithOne(x => x.Person)
        .onDelete(DeleteBehavior.Cascade);
});

你可以看到在代码中我们定义了一个 person 有多个 address, 通过 Addresses 属性. 并且一个地址有一个单一的 person 对应, 通过 Person 属性, 在 Address 实体上. 最后, 我们定义删除行为为 级联.

客户端设置 null 的删除行为 (Client Set Null Delete Behavior)

客户端设置 null 的删除行为会将所有外键属性设置为 null, 来代替删除子实体的行为. 该方法只会在内存中将外键属性设置为 null. 你依旧需要显式的调用 SaveChanges 方法来将删除持久化到数据库中. 为了在代码中使用这个模式, 我们需要在 Address 实体类中, 将属性 PersonId 设置为可空. 如下面代码:

namespace EFCore5WebApp.Core.Entities
{
    public class Address
    {
        public int Id { get; set; }
        public string AddressLine1 { get; set; }
        public string AddressLine2 { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Country { get; set; }
        public string ZipCode { get; set; }
        public int? PersonId { get; set; }
        public Person Person { get; set; }
    }
}

下一步需要在 AppDbContext 的 OnModelCreating 方法中设置删除模式为 ClientSetNull, 代码如下:

modelBuilder.Entity<Person>(entity =>
{
    entity.HasMany(x => x.Addresses)
        .WithOne(x => x.Person)
        .HasForeignKey(x => x.PersonId)
        .OnDelete(DeleteBehavior.ClientSetNull);
});

如你在代码示例中看到的, 我们还显式设置了外键属性为 PersonId, 来辅助 Entity Framework Core, 以便它知道需要设置哪一个属性值为 null.

限制删除行为 (restrict delete behavior)

限制删除行为不执行级联删除. 它需要你在代码中手动的完成子实体的删除逻辑. 要使用限制删除行为, 我们需要更新 AppDbContext 类中的 OnModelCreating 方法. 来设置 Person 实体中的 Addresses 的删除行为为 Restrict. 代码如下:

modelBuilder.Entity<Person>(entity =>
{
    entity.HasMany(x => x.Addresses)
        .WithOne(x => x.Person)
        .HasForeignKey(x => x.PersonId)
        .OnDelete(DeleteBehavior.Restrict);
});

你可以看到, 示例代码与客户端设置 null 的删除行为几乎一样, 除了删除行为的枚举值 DeleteBehavior.Restrict 不同.

设置为 null 的删除行为 (Set Null Delete Behavior)

设置为 null 的删除行为与客户端设置 null 的行为相同, 除了父实体在删除时, 外键属性会自动设置为 null 而不需要你来人工介入之外. 看下面的代码, 我们需要在 AppDbContext 中的 OnModelCreating 方法中进行如下处理:

modelBuilder.Entity<Person>(entity =>
{
    entity.HasMany(x => x.Addresses)
        .WithOne(x => x.Person)
        .HasForeignKey(x => x.PersonId)
        .OnDelete(DeleteBehavior.SetNull);
});

你可以看到, 代码与 ClientSetNull 一样, 除了在链式方法 OnDelete() 中我们传入的参数值 DeleteBehavior.SetNull 不同之外.

集成测试

现在, 我已经知道了如何使用 Entity Framework Core 5 来删除数据了, 下面我们为该操作添加一个集成测试. 打开 DAL.Tests 项目, 并创建一个新类 DeleteTests. 看下面单元测试类:

被吐槽的地方, 代码随意的修改, 突然冒出 CreatedOn 属性.

using EFCore5WebApp.Core.Entities;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace EFCore5WebApp.DAL.Tests
{
    [TestFixture]
    public class DeleteTests
    {

        private AppDbContext _context;

        [SetUp]
        public void SetUp() {
            _context = new AppDbContext(
                    new DbContextOptionsBuilder<AppDbContext>()
                    .UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EfCore5WebApp;Trusted_Connection=True;MultipleActiveResultSets=true")
                    .Options
                );

            // 添加一个 Person 记录
            var record = new Person()
            { 
                FirstName = "Clarke",
                LastName = "Kent",
                CreatedOn = DateTime.Now,
                EmailAddress = "clarke@daileybugel.com",
                Addresses = new List<Address>() 
                { 
                    new Address
                    { 
                        AddressLine1 = "1234 Fake Street",
                        AddressLine2 = "Suite 1",
                        City = "Chicage",
                        State = "IL",
                        ZipCode = "60652",
                        Country = "United States"
                    },
                    new Address
                    { 
                        AddressLine1 = "555 Waverly Street",
                        AddressLine2 = "APT B2",
                        City = "Mt. Pleasant",
                        State = "MI",
                        ZipCode = "48858",
                        Country = "USA"
                    }
                }
            };

            _context.Persons.Add(record);
            _context.SaveChanges();
        }

        [Test]
        public void DeletePerson()
        {
            var existing = _context.Persons.Single(x => x.FirstName == "Clarke" && x.LastName == "Kent");
            var personId = existing.Id;
            _context.Persons.Remove(existing);
            _context.SaveChanges();

            var found = _context.Persons.SingleOrDefault(x => x.FirstName == "Clarke" && x.LastName == "Kent");
            Assert.IsNull(found);

            var addresses = _context.Addresses.Where(x => x.PersonId == personId);
            Assert.AreEqual(0, addresses.Count());
        }
    }
}

删除模式这里未知, 经测试后似乎没有什么变化, 仅级联会删除地址数据, 其他情况都是将关联的 PersonId 设置为 NULL. 待测试.

类似于其他单元测试, 我们首先在 SetUp 方法中连接到 SQL Server 中. 下一步, 我添加一个 Person 记录, 并带有两个地址记录. 在 DeletePerson 方法中, 我们通过查询 person 记录来测试删除功能. 我们移除记录, 然后提交修改. 移除后, 我们确保它无法再检索到, 即地址已被删除.

需要补充的地方:

  • 不同是删除模型的异同, 以及代码逻辑的演示. [不知是否操作有误, 没有察觉出客户端置空, 限制删除, 以及置空的区别]
  • 在 OnModelCreating 配置实体关系的方法的作用, 以及会有什么影响.
  • 实际还有, 那些属性会创建数据库字段等.

小结

本章中, 我们介绍了如何删除根实体, 以及其子实体. 默认情况下, EFCore5 会删除子实体, 在删除根实体时. 同时介绍了, 如果需要如何修改默认的行为. 然后, 我介绍如何测试这个功能, 通过使用一个集成测试. 下一章中, 我会深入介绍导航属性.

深入介绍??? 都不太信任作者了...

Last Updated:
Contributors: jk