How to use Ecto optimistic_lock with Phoenix generated update action?

Hello,
I’ve been trying to implement Ecto.Changeset.optimistic_lock in a Phoenix application but I’m stuck with the generated controller’s update action.

In iex I have success rising an Ecto.StaleEntryError when I try to update a stale record, (as shown in the documentation):

iex> post = Repo.insert!(%Post{title: "foo"})
%Post{id: 1, title: "foo", lock_version: 1}
iex> valid_change = Post.changeset(:update, post, %{title: "bar"})
iex> stale_change = Post.changeset(:update, post, %{title: "baz"})
iex> Repo.update!(valid_change)
%Post{id: 1, title: "bar", lock_version: 2}
iex> Repo.update!(stale_change)
** (Ecto.StaleEntryError) attempted to update a stale entry

here the two changeset are being generated from the same post struct, and Repo.update raise the expected error.

On the other hand, when the update is performed concurrently in two browser windows, -my understanding is that- the controller’s update action performs a new get_post!(id) and then uses that struct to make the update’s changeset, with the form params; therefore the latest update is always overwriting the first occurring, thus making optimistic_lock useless in this context.

I couldn’t find much help on this issue, and it make me think I’m getting all this wrong… Thank’s for any help.

That’s an interesting question. Disclaimer: I’ve never used optimistic_lock.

Since lock_version is just a regular field, one idea would be to pass, alongside with the ID, the version of the object in the update method and using that version before you apply any changes.

Example, in your form:

<form action="<%= Routes.post_path(@conn, :update, @post) %>" method="POST">
  ...
  <input type="hidden" name="post[lock_version]" value="<%= @post.lock_version %>"> 
</form>

And in your controller:

def update(conn, %{"id" => id, "post" => %{"lock_version" => lock_version}}) do
  post = 
    id
    |> Posts.get_post!()
    |> Map.put(:lock_version, lock_version)

  ...
end

Now you will will get a StaleEntryError if you’re updating an old post.

EDIT: you might want to use hidden_input to generate the hidden field instead of doing it manually

2 Likes

Hello 1player and thank you,

Since lock_version is just a regular field, one idea would be to pass, alongside with the ID, the version of the object in the update method and using that version before you apply any changes […]

post = 
  id
  |> Posts.get_post!()
  |> Map.put(:lock_version, lock_version)

Oh :astonished:, right, that easy :man_facepalming:

Replacing the validly-updated new :lock_version with the stale one after get_post!(id)… I’m going to try this now, sure it is going to work!

I’ve been stubbornly poking around with attempts to pass the post struct assigned in edit, from the form to the update action, with no success, and I couldn’t think simple again. Thank you for your help!

Hello @patrickdm! I was wondering if you could share your approach to managing the Ecto.StaleEntryError exception within a controller or a live view?

I came across a similar issue discussed in another thread during my research, but unfortunately, it didn’t have any responses. Your insights would be greatly appreciated.

https://groups.google.com/g/phoenix-talk/c/koVPStsHJ1k

After conducting further research, I believe I’ve found a suitable solution to my question, particularly in relation to LiveView.

Here’s the section I found in the docs: Error and exception handling — Phoenix LiveView v0.20.7