# How to preven concurrent writes ```elixir Mix.install([{:ash, "~> 3.0.0-rc"}], consolidate_protocols: false) # Set to `:debug` if you want to see ETS logs Logger.configure(level: :warning) ``` ## Preventing Concurrent Writes Often, when working with resources, we want to ensure that a record has not been edited since we last read it. We may want this for UX reasons, or for ensuring data consistency, etc. To ensure that a record hasn't been updated since the last time we read it, we use Optimistic Locking. ## Adding Optimistic Locking - Add a `:version` attribute to your resource. - This will typically be an `:integer` - with a default of `1` - and `allow_nil?: false` - If you want optimistic locking to occur for _specific actions_: - Add `change optimistic_lock(:version)` to those specific actions - If you want optimistic locking to occur for _all actions_: - Add a global `changes` block, if you do not have one - Add `change optimistic_lock(:version), on: [:create, :update, :destroy]` - Note that we list the action types, because `:destroy` is not in the list of action types a change runs on by default. - If you want to apply optimistic locking to _many but not all actions_: - Add a global `changes` block, if you do not have one - Add `change optimistic_lock(:version), where: action_is([:list, :of, :actions])` ## Need more context? See the documentation for `Ash.Resource.Change.OptimisticLock` ## Examples ```elixir defmodule Address do use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets attributes do uuid_primary_key :id attribute :version, :integer, allow_nil?: false, default: 1 attribute :state, :string, allow_nil?: false attribute :county, :string, allow_nil?: false end actions do defaults [:read, create: [:state, :county]] update :update do accept [:state, :county] change optimistic_lock(:version) end end # apply to all actions # changes do # change optimistic_lock(:version) # end # apply to a list of actions # changes do # change optimistic_lock(:version), where: action_is([:list, :of, :actions]) # end end defmodule Domain do use Ash.Domain, validate_config_inclusion?: false resources do resource Address do define(:get_address, action: :read, get_by: [:id]) define(:create_address, action: :create, args: [:state, :county]) define(:update_address, action: :update) end end end ``` ``` {:module, Domain, <<70, 79, 82, 49, 0, 2, 14, ...>>, [ Ash.Domain.Dsl.Resources.Resource, Ash.Domain.Dsl.Resources.Options, Ash.Domain.Dsl, %{opts: [], entities: [...]}, Ash.Domain.Dsl, Ash.Domain.Dsl.Resources.Options, ... ]} ``` ## Triggering a StaleRecord error ```elixir address = Domain.create_address!("FL", "Pinellas") Domain.update_address!(address, %{state: "NC", county: "Guilford"}) # `address` still has a version of `1`, so our optimistic lock should catch it! Domain.update_address(address, %{county: "Miami-Dade"}) ``` ``` {:error, %Ash.Error.Invalid{ changeset: nil, query: nil, action_input: nil, errors: [ %Ash.Error.Changes.StaleRecord{ resource: "Address", filter: %{"version" => 1}, splode: Ash.Error, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid } ], splode: Ash.Error, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid }} ``` ## Refetching a record to get the latest version ```elixir address = Domain.create_address!("FL", "Pinellas") Domain.update_address!(address, %{state: "NC", county: "Guilford"}) case Domain.update_address(address, %{county: "Miami-Dade"}) do {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Changes.StaleRecord{}]}} -> # In a liveview, you wouldn't just refetch and resubmit # you would show the user an error and have them submit the form again address = Domain.get_address!(address.id) Domain.update_address!(address, %{county: "Miami-Dade"}) {:ok, domain} -> domain end ``` ``` #Address< __meta__: #Ecto.Schema.Metadata<:loaded>, id: "9f5d247e-fc44-41d1-80b4-8553c63855bb", version: 3, state: "NC", county: "Miami-Dade", aggregates: %{}, calculations: %{}, ... > ```