mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
166 lines
4 KiB
Markdown
166 lines
4 KiB
Markdown
<!-- livebook:{"persist_outputs":true} -->
|
|
|
|
# Optimistic Locking
|
|
|
|
```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)
|
|
```
|
|
|
|
## Steps to add optimistic locking to an `Ash.Resource`
|
|
|
|
* 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)`
|
|
* 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`
|
|
|
|
## Define a resource with a version attribute
|
|
|
|
<!-- 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)
|
|
# 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
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
{: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"})
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
{: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
|
|
```
|
|
|
|
<!-- livebook:{"output":true} -->
|
|
|
|
```
|
|
#Address<
|
|
__meta__: #Ecto.Schema.Metadata<:loaded>,
|
|
id: "9f5d247e-fc44-41d1-80b4-8553c63855bb",
|
|
version: 3,
|
|
state: "NC",
|
|
county: "Miami-Dade",
|
|
aggregates: %{},
|
|
calculations: %{},
|
|
...
|
|
>
|
|
```
|