Ecto.Repo.update_all 用于原子更新?

Ecto.Repo.update_all for atomic updates?

Phoenix's Contexts guide 中有一个部分向虚拟 CMS 上下文添加了页面浏览量增加功能。在 CMS 上下文中创建的函数如下所示:

def inc_page_views(%Page{} = page) do
  {1, [%Page{views: views}]} =
    from(p in Page, where: p.id == ^page.id, select: [:views])
    |> Repo.update_all(inc: [views: 1])

  put_in(page.views, views)
end

换句话说,inc_page_views 采用 Page 结构,使用其 id 查找相应的数据库记录,使用 Repo.update_all 自动增加视图计数(参见交错示例的文档),确保仅更新了 1 条记录,并且 returns 具有更新视图计数的新 Page

为什么这个例子使用Ecto.Repo.update_all/3 rather than Ecto.Repo.update/2?因为我们知道我们只想对一条记录进行操作,所以可能会更新一堆记录并追溯检查我们没有更新,而不是更新特定的 Ecto.Changeset,这感觉很奇怪,这可能看起来像这样:

def inc_page_views(%Page{views: curr_views} = page) do
  page
  |> Page.changeset(%{views: curr_views + 1})
  |> Repo.update()
end

此实现是 shorter/simpler,但我猜 Phoenix 文档作者没有使用它是有充分理由的。我的直觉是 Repo.update 版本一定缺少 Repo.update_all 版本中应该存在的原子更新 属性,但我不知道为什么!有人可以帮助解释这些实现之间的区别以及为什么文档可能选择第一个吗?

def inc_page_views(%Page{views: curr_views} = page) do
  page
  |> Page.changeset(%{views: curr_views + 1})
  |> Repo.update()
end

它引入了竞争条件。想象一下,您从数据库中获取页面并且它的 views 等于 5。然后,当您在 运行 上面的函数时,来自其他进程的其他数据库连接可能会将值从 5 更改为6. 但是由于这个函数不知道它,它仍然会将 5(现在已过时的值)加 1,并将值 6.

写入数据库

因此,正确的值不是 7,而是 6。

防止这种情况的方法是使用数据库锁来做类似的事情:

Page
|> where(id: ^id)
|> lock("FOR UPDATE")
|> Repo.one!()
|> inc_page_views()

或者简单地使用确保操作是原子的Repo.update_all