Later: I have realized this applies to validation errors, not constraints, as I’d originally thought. See footnote [3]
I have a changeset with both a constraint and optimistic locking:
%Animal{id: id}
|> cast(attrs, [:name, :lock_version])
|> unique_constraint(:name, name: "unique_available_names")
|> optimistic_lock(:lock_version)
IF the unique_constraint
is violated, I get an {:error, changeset}
back, but the changeset contains an incremented version of the :lock_version
. Since the update didn’t happen, the version in the changeset will be used to populate the edit form, which uses a hidden
input to send back the lock version:
<%= form_for @changeset, ...
<%= text_input(f, :name) %> ...
<%= hidden_input(f, :lock_version) %> <<<<<<<<<<<
A corrected update attempt will now fail with a StaleEntryError
.[1]
Am I misunderstanding the right way to use optimistic locking?[2] Details below.
[1] I assume the error check uses equality rather than >=
, but I only traced the code to Ecto.Adapters.Sql.schema, which is not far enough.
[2] This seems a different problem than 25570.
[3] I’ve confirmed that this also happens with a validate_length
error. That is, the :lock_version
is set to 2 in the error changeset, even though the :lock_version
in the on-disk copy is 1.
full changeset: #Ecto.Changeset<
action: :update,
changes: %{lock_version: 2, name: "preexisting"}, <<<<<<<<<<<<<<
errors: [
name: {"should be %{count} character(s)",
[count: 3838, validation: :length, kind: :is, type: :string]}
],
data: #Crit.Usables.Animal<>,
valid?: false
>
Start with two animals:
%Crit.Usables.Animal{
available: true,
id: 48789,
lock_version: 1,
name: "Original Bossie",
}
%Crit.Usables.Animal{
id: 48790,
lock_version: 1,
name: "preexisting",
}
The form will send back these params:
%{"lock_version" => "1", "name" => "preexisting"}
However, the changeset pipeline shown above transforms the lock version before it hits Repo.update
:
update: #Ecto.Changeset<
action: nil,
changes: %{lock_version: 2, name: "preexisting"},
errors: [],
data: #Crit.Usables.Animal<>,
valid?: true
>
… and the resulting error changeset contains the updated :lock_version
:
#Ecto.Changeset<
action: :update,
changes: %{lock_version: 2, name: "preexisting"}, <<<<<<<<<<<<<<
errors: [
name: {"has already been taken",
[constraint: :unique, constraint_name: "unique_available_names"]}
],
data: #Crit.Usables.Animal<>,
valid?: false
>
Might be worth noting that the original Animal
(changeset.data
) has not been changed.