mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 21:43:02 +12:00
162 lines
4.1 KiB
Markdown
162 lines
4.1 KiB
Markdown
<!-- livebook:{"persist_outputs":true} -->
|
|
|
|
# Optimistic Locking
|
|
|
|
```elixir
|
|
Mix.install([{:ash, "~> 3.0.0-rc"}], consolidate_protocols: false)
|
|
Logger.configure(level: :warning)
|
|
```
|
|
|
|
## What is optimistic locking?
|
|
|
|
Optimistic Locking is the process of only allowing an update to occur if the version of a record that you have in memory is the same as the version in the database. Otherwise, an error is returned.
|
|
|
|
Optimistic locking may used for two primary purposes:
|
|
|
|
### User Experience
|
|
|
|
For example, if a user is editing a form that contains `State` and `County` fields, and they change the `County`, while another user has used the form to change the `State`, you could end up with a mismatch between `State` and `County`.
|
|
|
|
With optimistic locking, the user will instead get an error message that the record has been changed since they last looked.
|
|
|
|
### Concurrency Safety
|
|
|
|
Optimistic locking can make actions safe to run concurrently even if they can't be performed in a single query(atomically), by returning an error if the resource in the data layer does not have the same version as the one being edited.
|
|
|
|
This tells the user that they need to reload and try again.
|
|
|
|
### Want to see how it works
|
|
|
|
Modify the setup block and configure the log level to debug to see logs from the ETS data layer.
|
|
|
|
<!-- livebook:{"force_markdown":true} -->
|
|
|
|
```elixir
|
|
Logger.configure(level: :debug)
|
|
```
|
|
|
|
## Define a resource with a version attribute
|
|
|
|
```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
|
|
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,
|
|
...
|
|
]}
|
|
```
|
|
|
|
## Trigger 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 jsut 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: "b4b1c187-6d0b-478a-a090-ec39cf58d114",
|
|
version: 3,
|
|
state: "NC",
|
|
county: "Miami-Dade",
|
|
aggregates: %{},
|
|
calculations: %{},
|
|
...
|
|
>
|
|
```
|