Ecto.Changeset.optimistic_lock

You're seeing just the function optimistic_lock, go back to Ecto.Changeset module for more information.
Link to this function

optimistic_lock(data_or_changeset, field, incrementer \\ &increment_with_rollover/1)

View Source

Specs

optimistic_lock(Ecto.Schema.t() | t(), atom(), (term() -> term())) :: t()

Applies optimistic locking to the changeset.

Optimistic locking (or optimistic concurrency control) is a technique that allows concurrent edits on a single record. While pessimistic locking works by locking a resource for an entire transaction, optimistic locking only checks if the resource changed before updating it.

This is done by regularly fetching the record from the database, then checking whether another user has made changes to the record only when updating the record. This behaviour is ideal in situations where the chances of concurrent updates to the same record are low; if they're not, pessimistic locking or other concurrency patterns may be more suited.

Usage

Optimistic locking works by keeping a "version" counter for each record; this counter gets incremented each time a modification is made to a record. Hence, in order to use optimistic locking, a field must exist in your schema for versioning purpose. Such field is usually an integer but other types are supported.

Examples

Assuming we have a Post schema (stored in the posts table), the first step is to add a version column to the posts table:

alter table(:posts) do
  add :lock_version, :integer, default: 1
end

The column name is arbitrary and doesn't need to be :lock_version. Now add a field to the schema too:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :lock_version, :integer, default: 1
  end

  def changeset(:update, struct, params \\ %{}) do
    struct
    |> Ecto.Changeset.cast(params, [:title])
    |> Ecto.Changeset.optimistic_lock(:lock_version)
  end
end

Now let's take optimistic locking for a spin:

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:

%Post{id: 1, title: "baz", lock_version: 1}

When a conflict happens (a record which has been previously fetched is being updated, but that same record has been modified since it was fetched), an Ecto.StaleEntryError exception is raised.

Optimistic locking also works with delete operations. Just call the optimistic_lock/3 function with the data before delete:

iex> changeset = Ecto.Changeset.optimistic_lock(post, :lock_version)
iex> Repo.delete(changeset)

optimistic_lock/3 by default assumes the field being used as a lock is an integer. If you want to use another type, you need to pass the third argument customizing how the next value is generated:

iex> Ecto.Changeset.optimistic_lock(post, :lock_uuid, fn _ -> Ecto.UUID.generate end)