ash/documentation/how-to/prevent-concurrent-writes.livemd

161 lines
4.4 KiB
Text
Raw Normal View History

<!-- livebook:{"persist_outputs":true} -->
2024-05-07 02:19:30 +12:00
# Prevent concurrent writes
```elixir
2024-05-11 07:28:31 +12:00
Mix.install([{:ash, "~> 3.0"}], consolidate_protocols: false)
# Set to `:debug` if you want to see ETS logs
2024-05-04 23:07:15 +12:00
Logger.configure(level: :warning)
```
2024-05-07 02:19:30 +12:00
## Introduction
2024-05-06 11:00:01 +12:00
2024-05-06 11:07:19 +12:00
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.
2024-05-06 11:00:01 +12:00
2024-05-06 11:07:19 +12:00
To ensure that a record hasn't been updated since the last time we read it, we use Optimistic Locking.
2024-05-06 11:00:01 +12:00
2024-05-07 02:19:30 +12:00
For more information, see the documentation for `Ash.Resource.Change.OptimisticLock`.
## Add Optimistic Locking
2024-05-06 11:00:01 +12:00
2024-05-07 02:52:17 +12:00
1. Add a `:version` attribute to your resource, of type `:integer`, with a default of `1` and `allow_nil?: false`
2024-05-07 02:48:32 +12:00
2. Add `change optimistic_lock(:version)`. Where you add it depends on which actions you want to prevent concurrent writes for:
* If you want optimistic locking to occur for _specific actions_:
1. Add `change optimistic_lock(:version)` to those specific actions
* If you want optimistic locking to occur for _all action types_:
1. Add a global `changes` block to the resource, if you do not have one
2. Add `change optimistic_lock(:version), on: [:create, :update, :destroy]`
* If you want to apply optimistic locking to _many but not all actions_:
1. Add a global `changes` block to the resource, if you do not have one
2. Add `change optimistic_lock(:version), where: action_is([:action1, :action2, :action3])`
2024-05-06 11:07:19 +12:00
## Examples
<!-- livebook:{"disable_formatting":true} -->
```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
2024-05-07 02:42:11 +12:00
# change optimistic_lock(:version), on: [:create, :update, :destroy]
# end
# apply to a list of actions
# changes do
2024-05-07 02:42:11 +12:00
# change optimistic_lock(:version), where: action_is([:action1, :action2, :action3])
# 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
```
<!-- livebook:{"output":true} -->
```
{:module, Domain, <<70, 79, 82, 49, 0, 2, 1, ...>>,
[
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!
2024-05-04 17:17:07 +12:00
Domain.update_address(address, %{county: "Miami-Dade"})
```
<!-- livebook:{"output":true} -->
2024-05-04 17:17:07 +12:00
```
{:error,
%Ash.Error.Invalid{
changeset: "#Changeset<>",
2024-05-04 17:17:07 +12:00
errors: [
%Ash.Error.Changes.StaleRecord{
resource: "Address",
filter: %{"version" => 1},
splode: Ash.Error,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
]
2024-05-04 17:17:07 +12:00
}}
```
## 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
```
<!-- livebook:{"output":true} -->
```
#Address<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "91e552e1-3307-4d68-969f-1b7e6651e695",
version: 3,
state: "NC",
county: "Miami-Dade",
aggregates: %{},
calculations: %{},
...
>
```