Ecto.Repo.update_all для атомарных обновлений? - PullRequest
1 голос
/ 31 октября 2019

В есть раздел руководства Phoenix по контекстам, в котором добавляются функциональные возможности просмотра страниц в фиктивном контексте 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? Поскольку мы знаем, что хотим работать только с 1 записью, кажется странным потенциально обновлять кучу записей и задним числом проверять, что мы этого не сделали, вместо того, чтобы обновлять конкретный Ecto.Changeset, который может выглядеть примерно так:

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 Ответ

1 голос
/ 31 октября 2019
def inc_page_views(%Page{views: curr_views} = page) do
  page
  |> Page.changeset(%{views: curr_views + 1})
  |> Repo.update()
end

это вводит условие гонки. Представьте, что вы получаете страницу из базы данных, и она имеет views, равный 5. Затем, пока вы выполняете вышеуказанную функцию, другое соединение БД из другого процесса может изменить значение с 5 на 6. Но так как эта функцияне знает об этом, он все равно добавит 1 к 5 (теперь устаревшее значение) и запишет в базу данных значение 6.

. В результате вместо правильного значения 7 выhave 6.

Чтобы предотвратить это, используйте блокировки базы данных, выполнив что-то вроде этого:

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

Или просто используйте Repo.update_all, который гарантирует, что операция является атомарной.

...