查缺补漏系统学习 EF Core 6 - 修改实体数据

这是 EF Core 系列的第六篇文章,上一篇文章讲述了 EF Core 中的原始 SQL 语句查询。

这篇文章讲一讲 EF Core 如何修改实体数据。

实体状态

在开始学习 EF Core 修改数据之前,我们必须要熟悉 EF Core 中的一些机制。.

前面的内容中,我们讲到 DbContext 包括三个属性:ChangeTrackerDatabase 和 Model

之前的示例中,Database 和 Model 我们都有用到过,那么 ChangeTracker 是做什么的?

ChangeTracker 属性提供了对当前加载的实体的,变化跟踪信息和操作的访问。

当我们想执行任何数据库修改操作时,无论是创建、修改还是删除一个实体,EF Core 都有关于跟踪和操作的信息。

为什么要保存这些信息呢?

因为在 EF Core 中,修改实体属性的值,并不会直接被保存到数据库中。

而是要我们调用 SaveChanges 方法,EF Core 才会将修改反应到数据库,在此之前,EF Core 不会执行任何操作。

因此,在调用 SaveChanges 方法之前, EF Core 需要知道我们都执行了什么操作,这对 EF Core 来说非常重要。

每一个被追踪的实体,都有附属于它的 State 属性。

当我们使用上下文对象加载实体,而不使用 AsNoTracking 方法时,或者我们通过 UpdateRemove 或 Add 方法,改变实体状态时,该实体都会成为被跟踪的实体。

状态属性的值可以通实体的 State 方法获得。

那么实体的状态有哪些呢?

  • 「Detached」 - 该实体没有被追踪,调用 SaveChanges 方法不会有任何效果
  • 「Unchanged」 - 该实体从数据库中加载,但没有任何变化。调用 SaveChanges 方法也不会有任何效果
  • 「Added」 - 该实体不存在于数据库中,调用 SaveChanges 方法会将其添加到数据库中。
  • 「Modified」 - 该实体存在于数据库中,并被修改过,因此,调用 SaveChanges 方法将在数据库中修改它
  • 「Deleted」 - 该实体存在于数据库中,调用 SaveChanges 方法,会将它从数据库中删除。

实体操作

接下来,让我们结合示例来演示增删改,并观察实体状态的变化:

var account = new Account
{
    Name = "张三",
    Age = 18
};
_context.Add(account);
_context.SaveChanges();

示例中,首先创建了一个 account 对象,此时它还未被附加到 EF Core 上下文中,它的状态应该是 Detached

当我们使用 Add 方法,将account对象作为实体添加到上下文后,它的状态应该是 Added

紧接着,使用 SaveChanges 方法,将新添加的实体,保存到数据库中,它的状态应该是 Unchanged

当我们再次修改实体中的任意属性时,它的状态会变成 Modified

// ...
account.Age = 20;
_context.SaveChanges();

使用 SaveChanges 方法,将已修改的实体,应用到数据库时,它的状态又会变回 Unchanged

最后,当我们从上下文中删除这个实体后,实体的状态会变成 Deleted

// ...
_context.Remove(account);
_context.SaveChanges();

再次使用 SaveChanges 方法,将实体从数据库中彻底删除,大家想一想,此时它的状态依然会变成什么?

这次可不是 Unchanged 了,因为它已经从上下文中被移除了,所以它的状态是 Detached

EF Core 正是用这么几个方法,对实体进行增删改。

当然,这只是其中一种方式。

在这个示例中,新增的对象经过 Add 方法,被添加到了上下文。

之后的更新和删除操作,都是针对实体已经存在于上下文中的情况。

那么如果,我们有一个对象,它不存在于上下文中,但它确实存在于数据库中,我们该如何对它就行更新或删除呢?

首先,你肯定不能用 Add 方法,虽然它可以将一个对象,作为实体添加到上下文中

但它还会使实体状态成为 Added,而不是 Modified,这种情况下执行 SaveChanges 方法,EF Core 只会生成插入数据的语句,肯定会造成数据库冲突,因为数据已存在。

比如,这个对象:

var account = new Account
{
    Id = new Guid("dd4feb0b-b57c-4338-a40a-7aa73fc6e460"),
    Name = "Zilor",
    Age = 99
};

实际开发中,它可能来自的客户端,这个对象的数据是经过之前的查询得到的。

现在客户端对它的属性进行了修改,主键肯定是不会变的。

此时,它的状态是 Detached,没有被附加到上下文中,我们该如何直接更新到数据库呢?

这里有两种方法可以做到:

一是先用 ID 从 EF Core 中查询出实体,然后用 account 对象中的属性值,修改实体中的值,再使用 SaveChanges 保存数据。

比如这样:

var dbAccount = _context.Accounts.FirstOrDefault(a => a.Id == account.Id);
dbAccount.Name = account.Name;
dbAccount.Age = account.Age;
_context.SaveChanges();

需要注意的是,如果修改的操作,没有造成实体值的任何变化,实体状态将仍是 Unchanged

此时,即便执行SaveChanges方法, 也不会有任何效果。

二是用附加方法,将 account 对象附加到上下文,然后手动修改它的状态,再使用 SaveChanges 保存数据。

比如这样:

var account = new Account
{
    Id = new Guid("dd4feb0b-b57c-4338-a40a-7aa73fc6e460"),
    Name = "Zilor",
    Age = 99
};

_context.Accounts.Attach(account);
_context.Entry(account).State = EntityState.Modified;
_context.SaveChanges();

Attach 方法用来附加实体,被附加的实体初始状态为 Unchanged,所以我们要手动修改实体状态。

执行应用,可以看到实体状态的流转。

第二种方式相对第一种方式,少了一步查询,也少了实体属性的赋值。

但是,无论实体的值相对于数据库是否有变化,更新操作都会执行。

这是方式适合全量更新,因为如果客户端传来的是不完整的对象,只包含了修改的属性,那么更新时可能会造成问题。

比如这个例子,如果 Account 对象中没有 Age 属性,更新到数据库时,Age 列的值会是个 「Null」

第一种方式,由于是从实体查询出来的数据,数据是完整的,修改也只是针对个别属性,所以保存数据时,不会发生这种情况。

而且第一种方式,可以判断出实体是否真的已经修改,无修改的话,不会执行任何操作。

所以,对于更新操作,建议使用第一种方式,更安全也更靠谱;

删除操作也是如此,只需要将实体状态修改为 Deleted,就可以直接删除,不够删除操作可以使用第二种方式。