【ASP.NET Core】让 EF Core 生成更“合理”的 UPDATE 语句

相信 .NET 玩家都知道,EF Core 自动产生 SQL 更新语句是通过跟踪实体状态实现的。不过,在 Web 中是无状态的,查询出一个东东返回到前台,然后用户要更新数据,POST 过来就是修改后的数据。

比较笨的方法是先根据主键查询出数据,然后把客户端 POST 过来的数据赋值,这样就有新旧值的比较了,EF Core 也能生成准确的 SET 子句了。这种方法虽然笨但很简单,它有个缺点:在更新前你会多跑一轮查询。

本文咱们就用这个实体来演示。

复制代码
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public string? Major { get; set; }
}
复制代码
public class DemoDbContext : DbContext
{
    public DemoDbContext(DbContextOptions<DemoDbContext> opt)
        : base(opt)
    {

    }

    public DbSet<Student> Students { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>(ent =>
        {
            ent.ToTable("tb_students");
            // 初始数据
            ent.HasData(new Student { Id = 1, Name = "吴小车", Major = "挖矿" },
                new Student { Id = 2, Name = "李肺", Major = "攻城掠地" },
                new Student { Id = 3, Name = "赵逗逗", Major = "打磨芯片" },
                new Student { Id = 4, Name = "范统", Major = "悲观经济学" },
                new Student { Id = 5, Name = "贾仁亿", Major = "社会猪圈学" });
        });
    }
}

Major 属性表示某同学选的专业。

这时候你会想到官方文档有介绍过的 ExecuteUpdate 方法。它不需要查询,可以直接发送 UPDATE 语句。

复制代码
_ = await dbcontext.Students.ExecuteUpdateAsync(setter =>
{
    setter.SetProperty(stu => stu.Name, newStu.Name);
    setter.SetProperty(stu => stu.Major, newStu.Major);
});

上述方法虽然避免了查询,但没有新旧值的比较,你不知道用户更改了哪些属性,只能全部 SET 一遍。其实,这种把所有字段都 SET 一遍的做法还更简单的操作------直接把实体状态变为 Modified 就完事了,EF Core 也会生成完整的 SET 子句。

复制代码
 public async Task<IActionResult> UpdateStudent(Student stu/*, string old*/)
 {
     var dbcontext = HttpContext.RequestServices.GetRequiredService<DemoDbContext>();
     // 开始跟踪
     dbcontext.Students.Attach(stu);
     // 强行改变实体状态
     dbcontext.Students.Update(stu);

     // 提交更新
     _ = await dbcontext.SaveChangesAsync();
     return RedirectToAction("Index");
 }

以前老周告诉过大伙伴:EF Core 从实体查询出来的时候开始跟踪。那是指正常情况,不正常情况下可以调用一下 Attach 方法。这个方法的功能就是把一个实体实例手动添加到 EF Core 的跟踪列表中,并且将实体状态设置为 Unchanged,即未更改。

随后,调用 Update 方法,会将实体状态变为 Modified,当然,EF Core 仍然不知道哪些属性被修改了,所以,除主键以外,其他属性都变为 Modified。

EF Core 生成的 SQL 语句如下:

复制代码
UPDATE "tb_students" SET "Major" = @p0, "Name" = @p1
      WHERE "Id" = @p2
      RETURNING 1;

尽管咱们没修改 Name 属性,但由于 EF Core 无法比较判断,所以连同 Name 属性也设置了。

那有没有办法让 EF Core 知道哪些属性被修改过呢?有的,EF Core 不仅允咱们改实体状态,也允许咱们手动设置旧值(快照值)和新值(当前值)。不过咱们要解决 HTTP 无状态的问题,即 HTTP 一轮请求后是不留痕迹的。因此,在用户请求数据时,在返回客户端时为实体对象做一个备份,用户编辑完了,连同备份的旧值一起 POST 回服务器,这样就能做比较了。

复制代码
@model DB.Student

<form class="modal-dialog" method="post" asp-action="@nameof(HomeController.UpdateStudent)">
    <input type="hidden" asp-for="Id" />
    @{
            // 把旧的值序列为 JSON
            string json = JsonSerializer.Serialize(Model);
    }
    <input type="hidden" name="old" value="@json"/>
    <div class="modal-content">
        <div class="modal-header">
            <h5>编辑学生信息</h5>
            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
            <div class="row mb-3">
                <label class="form-label" asp-for="Name">姓名:</label>
                <input type="text" class="form-control" asp-for="Name" />
            </div>
            <div class="row mb-3">
                <label class="form-label" asp-for="Major">专业:</label>
                <input class="form-control" type="text" asp-for="Major" />
            </div>
        </div>
        <div class="modal-footer">
            <button type="button" data-bs-dismiss="modal" class="btn btn-secondary">取消</button>
            <button type="submit" class="btn btn-primary">确定</button>
        </div>
    </div>
</form>

在 form 中,咱们只要用一个隐藏字段保存旧的数据(JSON)就行,然后在接收 POST 的 action 方法中加上名为 old 的字符串参数。

复制代码
public async Task<IActionResult> UpdateStudent(Student stu, string old)
{
    var dbcontext = HttpContext.RequestServices.GetRequiredService<DemoDbContext>();
    // 开始跟踪
    dbcontext.Students.Attach(stu);
    var entry = dbcontext.Students.Entry(stu);
    // 反序列化
    var oldvals = JsonSerializer.Deserialize<Student>(old);
    // 重点:设置快照
    entry.OriginalValues.SetValues(oldvals!);
    // 提交更新
    _ = await dbcontext.SaveChangesAsync();
    return RedirectToAction("Index");
}

全场最重要的就是 OriginalValues.SetValues 这句,就是把原来备份的数据作为快照,它就能跟新的值比较了。

再来看看效果。

现在,咱们只改变了学生的专业名称。然后提交。

看看 EF Core 生成的 SQL 语句。

复制代码
UPDATE "tb_students" SET "Major" = @p0
      WHERE "Id" = @p1
      RETURNING 1;

由于现在它能识别到只有 Major 属性的值改变了,所以 SET 子句只更新这个属性。

这样解决是不是很舒服?不会丧失 EF Core 自动生成 SQL 的功能,又不用自己拼装 SQL 语句。

这次分享的内容比较简单,所以就到这里了。至于大伙伴可能疑惑老周又跑去干什么大项目了,这么久没写文章了。想多了,大项目不会找老周这种乡下码农的,人家肯定找那些满屋子放满证书的靓仔。

最近是搞了个 Stm32 的小项目,也没多复杂,只用了 9 天时间。主要是前期去他们工厂跟他们吵了四五天,最后才确定方案。而且 Boss 还想用 500 块钱打发的态度,还没脑子的瞎指挥(这货就是啥也不懂还乐于装 X 那种)。还是人家厂里的经理懂事,项目能搞成这小伙子的功劳不小,两边讲道理。老周一开始就给 boss 放下狠话的,你要是几百块能打发的你自己用 AI 做,不必去找人来干;只要你找到我给做,我可以按你们的需求做,但你得按我的思路来,不要在那里瞎叫喝。你去打听打听,我当年刚大学毕业,面试就敢硬刚 HR,把 HR 怼到辞职的。你,还排不上号。