删除数据
本章中, 我会介绍使用 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 会删除子实体, 在删除根实体时. 同时介绍了, 如果需要如何修改默认的行为. 然后, 我介绍如何测试这个功能, 通过使用一个集成测试. 下一章中, 我会深入介绍导航属性.
深入介绍??? 都不太信任作者了...