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

时间:2019-10-31 06:09:24

标签: elixir phoenix-framework ecto

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条记录,并返回具有更新视图计数的新Page

为什么此示例使用Ecto.Repo.update_all/3而不是Ecto.Repo.update/2?既然我们知道我们只想处理一条记录,那么可能会更新一堆记录并回溯地检查我们是否有记录,而不是更新特定的Ecto.Changeset,这看起来很奇怪: / p>

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

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

1 个答案:

答案 0 :(得分:1)

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

它引入了竞争条件。想象一下,您从数据库获取页面,并且该页面的views等于5。然后,当您运行上面的函数时,来自另一个进程的另一个db连接可能会将值从5更改为6。该函数对此一无所知,它仍将5加1(现在已过时的值),并将值6写入数据库。

结果,不是正确的值7,而是6。

防止这种情况发生的方法是使用数据库锁执行类似的操作:

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

或者只需使用Repo.update_all来确保操作是原子的。