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
[Paraphrasing,inc_page_views
接受Page
结构,使用其id
查找相应的数据库记录,使用Repo.update_all
原子地增加视图计数(请参阅docs以获取交错示例),确保仅1记录已更新,并返回具有更新视图计数的新Page
。
为什么此示例使用Ecto.Repo.update_all/3
而不是Ecto.Repo.update_all/3
?既然我们知道我们只想对1条记录进行操作,那么可能会更新一堆记录并回溯地检查我们是否有记录,而不是更新特定的Ecto.Repo.update/2
,这看起来很奇怪:
Ecto.Repo.update/2
此实现更短/更简单,但是我猜Phoenix的文档编写者没有充分理由使用它。我的直觉是,Ecto.Changeset
版本必须缺少def inc_page_views(%Page{views: curr_views} = page) do
page
|> Page.changeset(%{views: curr_views + 1})
|> Repo.update()
end
版本中应该存在的原子更新属性,但是我不知道为什么!有人可以帮忙解释一下这些实现之间的区别以及为什么文档可能会选择第一个吗?
Repo.update
它引入了竞争条件。假设您从数据库中获取页面,并且页面具有Repo.update_all
等于5。然后,当您运行上面的函数时,来自另一个进程的另一个db连接可能会将值从5更改为6。但是由于此函数不知道它,它仍然会将1加到5(现在已经过时的值),并将值def inc_page_views(%Page{views: curr_views} = page) do
page
|> Page.changeset(%{views: curr_views + 1})
|> Repo.update()
end
写入数据库。
因此,不是正确的值7,而是6。
防止这种情况的方法是使用数据库锁执行类似的操作:
views
或者简单地使用
6
确保操作是原子的。