mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 21:43:02 +12:00
160 lines
4.4 KiB
Markdown
160 lines
4.4 KiB
Markdown
<!-- livebook:{"persist_outputs":true} -->
|
|
|
|
# Prevent concurrent writes
|
|
|
|
```elixir
|
|
Mix.install([{:ash, "~> 3.0"}], consolidate_protocols: false)
|
|
# Set to `:debug` if you want to see ETS logs
|
|
Logger.configure(level: :warning)
|
|
```
|
|
|
|
## Introduction
|
|
|
|
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.
|
|
|
|
For more information, see the documentation for `Ash.Resource.Change.OptimisticLock`.
|
|
|
|
## Add Optimistic Locking
|
|
|
|
1. Add a `:version` attribute to your resource, of type `:integer`, with a default of `1` and `allow_nil?: false`
|
|
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])`
|
|
|
|
## 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
|
|
# change optimistic_lock(:version), on: [:create, :update, :destroy]
|
|
# end
|
|
|
|
# apply to a list of actions
|
|
# changes do
|
|
# 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!
|
|
Domain.update_address(address, %{county: "Miami-Dade"})
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
{:error,
|
|
%Ash.Error.Invalid{
|
|
changeset: "#Changeset<>",
|
|
errors: [
|
|
%Ash.Error.Changes.StaleRecord{
|
|
resource: "Address",
|
|
filter: %{"version" => 1},
|
|
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
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
#Address<
|
|
__meta__: #Ecto.Schema.Metadata<:loaded>,
|
|
id: "91e552e1-3307-4d68-969f-1b7e6651e695",
|
|
version: 3,
|
|
state: "NC",
|
|
county: "Miami-Dade",
|
|
aggregates: %{},
|
|
calculations: %{},
|
|
...
|
|
>
|
|
```
|