View Source Ash.Changeset (ash v3.4.1)
Changesets are used to create and update data in Ash.
Create a changeset with new/1
or new/2
, and alter the attributes
and relationships using the functions provided in this module. Nothing in this module
actually incurs changes in a data layer. To commit a changeset, see Ash.create/2
and Ash.update/2
.
Changeset lifecycle
Action Lifecycle
The following example illustrates the hook lifecycle of a changeset.
defmodule AshChangesetLifeCycleExample do
def change(changeset, _, _) do
changeset
# execute code both before and after the transaction
|> Ash.Changeset.around_transaction(fn changeset, callback ->
callback.(changeset)
end)
# execute code before the transaction is started. Use for things like external calls
|> Ash.Changeset.before_transaction(fn changeset -> changeset end)
# execute code in the transaction, before and after the data layer is called
|> Ash.Changeset.around_action(fn changeset, callback ->
callback.(changeset)
end)
# execute code in the transaction, before the data layer is called
|> Ash.Changeset.before_action(fn changeset -> changeset end)
# execute code in the transaction, after the data layer is called, only if the action is successful
after_action(fn changeset, result -> {:ok, result} end)
# execute code after the transaction, both in success and error cases
|> Ash.Changeset.after_transaction(fn changeset, success_or_error_result -> success_or_error_result end
end
end
Summary
Functions
Returns a list of attributes, aggregates, relationships, and calculations that are being loaded
Adds an error to the changesets errors list, and marks the change as valid?: false
.
Adds an after_action hook to the changeset.
Adds an after_transaction hook to the changeset. Cannot be called within other hooks.
Returns the original data with attribute changes merged, if the changeset is valid.
Adds an around_action hook to the changeset.
Adds an around_transaction hook to the changeset.
Gets a reference to a field, or the current atomic update expression of that field.
Adds multiple atomic changes to the changeset
Adds an atomic change to the changeset.
Checks if an attribute is not nil, either in the original data, or that it is not being changed to a nil
value if it is changing.
Adds a before_action hook to the changeset.
Adds a before_transaction hook to the changeset.
Adds a change to the changeset, unless the value matches the existing value.
Calls change_attribute/3
for each key/value pair provided.
The same as change_attribute
, but annotates that the attribute is currently holding a default value.
Change an attribute only if is not currently being changed
Change an attribute if is not currently being changed, by calling the provided function.
Returns true if an attribute exists in the changes
Returns true if any attributes on the resource are being changed.
Returns true if a relationship exists in the changes
Clears an attribute or relationship change off of the changeset.
Remove an argument from the changeset
Ensure the the specified attributes are nil
in the changeset results.
Ensures that the given attributes are selected.
Turns the special case {:replace, fields}, :replace_all and {:replace_all_except, fields} upsert_fields options into a list of fields
Fetches the value of an argument provided to the changeset or :error
.
Gets the value of an argument provided to the changeset, falling back to Ash.Changeset.fetch_change/2
if nothing was provided.
Gets the new value for an attribute, or :error
if it is not being changed.
Adds a filter for a record being updated or destroyed.
Constructs a changeset for a given action, and validates it.
Constructs a changeset for a given create action, and validates it.
Constructs a changeset for a given destroy action, and validates it.
Constructs a changeset for a given update action, and validates it.
Changes an attribute even if it isn't writable
Calls force_change_attribute/3
for each key/value pair provided.
Force change an attribute if it is not currently being changed.
Force change an attribute if it is not currently being changed, by calling the provided function.
Add an argument to the changeset, which will be provided to the action.
Merge a map of arguments to the arguments list.
Gets the value of an argument provided to the changeset.
Gets the value of an argument provided to the changeset, falling back to Ash.Changeset.get_attribute/2
if nothing was provided.
Gets the changing value or the original value of an attribute.
Gets the original value for an attribute
Sets a custom error handler on the changeset.
Calls the provided load statement on the result of the action at the very end of the action.
Returns true if the field/relationship or path to field/relationship is being loaded.
Manages the related records by creating, updating, or destroying them as necessary.
Returns a new changeset over a resource.
Checks if an argument is not nil or an attribute is not nil, either in the original data, or that it is not being changed to a nil
value if it is changing.
Puts a key/value in the changeset context that can be used later.
Ensure that only the specified attributes are present in the results.
Add an argument to the changeset, which will be provided to the action.
Merge a map of arguments to the arguments list.
Deep merges the provided map into the changeset context that can be used later.
Set the result of the action. This will prevent running the underlying datalayer behavior
Updates an existing attribute change by applying a function to it.
Wraps a function in the before/after action hooks of a changeset.
Types
@type after_action_fun() :: (t(), Ash.Resource.record() -> {:ok, Ash.Resource.record()} | {:ok, Ash.Resource.record(), [Ash.Notifier.Notification.t()]} | {:error, any()})
@type after_transaction_fun() :: (t(), {:ok, Ash.Resource.record()} | {:error, any()} -> {:ok, Ash.Resource.record()} | {:error, any()})
@type around_action_callback() :: (t() -> around_action_result())
@type around_action_fun() :: (t(), around_action_callback() -> around_action_result())
@type around_action_result() :: {:ok, Ash.Resource.record(), t(), %{notifications: [Ash.Notifier.Notification.t()]}} | {:error, Ash.Error.t()}
@type around_transaction_callback() :: (t() -> around_transaction_result())
@type around_transaction_fun() :: (t(), around_transaction_callback() -> around_transaction_result())
@type around_transaction_result() :: {:ok, Ash.Resource.record()} | {:error, any()}
@type before_action_fun() :: (t() -> t() | {t(), %{notifications: [Ash.Notifier.Notification.t()]}})
@type manage_relationship_type() ::
:append_and_remove | :append | :remove | :direct_control | :create
@type phase() ::
:around_transaction
| :around_action
| :after_transaction
| :before_transaction
| :after_action
| :before_action
| :validate
| :pending
| :atomic
@type t() :: %Ash.Changeset{ __validated_for_action__: atom() | nil, action: Ash.Resource.Actions.action() | nil, action_failed?: boolean(), action_type: Ash.Resource.Actions.action_type() | nil, added_filter: Ash.Filter.t() | nil, after_action: [after_action_fun() | {after_action_fun(), map()}], after_transaction: [ after_transaction_fun() | {after_transaction_fun(), map()} ], arguments: %{optional(atom()) => any()}, around_action: [around_action_fun() | {around_action_fun(), map()}], around_transaction: [ around_transaction_fun() | {around_transaction_fun(), map()} ], atomic_after_action: term(), atomic_changes: term(), atomic_validations: term(), atomics: Keyword.t(), attribute_changes: term(), attributes: %{optional(atom()) => any()}, before_action: [before_action_fun() | {around_action_fun(), map()}], before_transaction: [ before_transaction_fun() | {before_transaction_fun(), map()} ], casted_arguments: term(), casted_attributes: term(), context: map(), context_changes: term(), data: Ash.Resource.record() | nil, defaults: [atom()], dirty_hooks: term(), domain: module() | nil, errors: [Ash.Error.t()], filter: Ash.Filter.t() | nil, handle_errors: nil | (t(), error :: any() -> :ignore | t() | (error :: any()) | {error :: any(), t()}), invalid_keys: MapSet.t(), load: keyword(keyword()), params: %{optional(atom() | binary()) => any()}, phase: phase(), relationships: %{ optional(atom()) => %{optional(atom() | binary()) => any()} | [%{optional(atom() | binary()) => any()}] }, resource: module(), select: [atom()] | nil, tenant: term(), timeout: pos_integer() | nil, to_tenant: term(), valid?: boolean() }
Functions
accessing(changeset, types \\ [:attributes, :relationships, :calculations, :attributes], only_public? \\ true)
View SourceReturns a list of attributes, aggregates, relationships, and calculations that are being loaded
Provide a list of field types to narrow down the returned results.
@spec add_error(t(), error_info() | [error_info()], Keyword.t()) :: t()
Adds an error to the changesets errors list, and marks the change as valid?: false
.
Error Data
The given errors
argument can be a string, a keyword list, a struct, or a list of any of the three.
If errors
is a keyword list, or a list of keyword lists, the following keys are supported in the keyword list:
field
(atom) - the field that the error is for. This is required, unlessfields
is given.fields
(list of atoms) - the fields that the error is for. This is required, unlessfield
is given.message
(string) - the error messagevalue
(any) - (optional) the field value that caused the error
@spec after_action( t(), after_action_fun(), Keyword.t() ) :: t()
Adds an after_action hook to the changeset.
Provide the option prepend?: true
to place the hook before all
other hooks instead of after.
@spec after_transaction( t(), after_transaction_fun(), Keyword.t() ) :: t()
Adds an after_transaction hook to the changeset. Cannot be called within other hooks.
after_transaction
hooks differ from after_action
hooks in that they are run
on success and failure of the action or some previous hook.
Provide the option prepend?: true
to place the hook before all
other hooks instead of after.
@spec apply_attributes(t(), opts :: Keyword.t()) :: {:ok, Ash.Resource.record()} | {:error, t()}
Returns the original data with attribute changes merged, if the changeset is valid.
Options:
- force? - applies current attributes even if the changeset is not valid
@spec around_action(t(), around_action_fun()) :: t()
Adds an around_action hook to the changeset.
Your function will get the changeset, and a callback that must be called with a changeset (that may be modified).
The callback will return {:ok, result, instructions}
or {:error, error}
. You can modify these values, but the
return value must be one of those types. Instructions contains the notifications in its notifications
key, i.e
%{notifications: [%Ash.Resource.Notification{}, ...]}
.
The around_action calls happen first, and then (after they each resolve their callbacks) the before_action
hooks are called, followed by the action itself occurring at the data layer and then the after_action
hooks being run.
Then, the code that appeared after the callbacks were called is then run.
For example:
changeset
|> Ash.Changeset.around_action(fn changeset, callback ->
IO.puts("first around: before")
result = callback.(changeset)
IO.puts("first around: after")
result
end)
|> Ash.Changeset.around_action(fn changeset, callback ->
IO.puts("second around: before")
result = callback.(changeset)
IO.puts("second around: after")
result
end)
|> Ash.Changeset.before_action(fn changeset ->
IO.puts("first before")
changeset
end, append?: true)
|> Ash.Changeset.before_action(fn changeset ->
IO.puts("second before")
changeset
end, append?: true)
|> Ash.Changeset.after_action(fn changeset, result ->
IO.puts("first after")
{:ok, result}
end)
|> Ash.Changeset.after_action(fn changeset, result ->
IO.puts("second after")
{:ok, result}
end)
This would print:
first around: before
second around: before
first before
second before
<-- action happens here
first after
second after
second around: after <-- Notice that because of the callbacks, the after of the around hooks is reversed from the before
first around: after
Warning: using this without understanding how it works can cause big problems.
You must call the callback function that is provided to your hook, and the return value must
contain the same structure that was given to you, i.e {:ok, result_of_action, instructions}
.
You can almost always get the same effect by using before_action
, setting some context on the changeset
and reading it out in an after_action
hook.
@spec around_transaction(t(), around_transaction_fun()) :: t()
Adds an around_transaction hook to the changeset.
Your function will get the changeset, and a callback that must be called with a changeset (that may be modified).
The callback will return {:ok, result}
or {:error, error}
. You can modify these values, but the return value
must be one of those types.
The around_transaction calls happen first, and then (after they each resolve their callbacks) the before_transaction
hooks are called, followed by the action hooks and then the after_transaction
hooks being run.
Then, the code that appeared after the callbacks were called is then run.
For example:
changeset
|> Ash.Changeset.around_transaction(fn changeset, callback ->
IO.puts("first around: before")
result = callback.(changeset)
IO.puts("first around: after")
result
end)
|> Ash.Changeset.around_transaction(fn changeset, callback ->
IO.puts("second around: before")
result = callback.(changeset)
IO.puts("second around: after")
result
end)
|> Ash.Changeset.before_transaction(fn changeset ->
IO.puts("first before")
changeset
end, append?: true)
|> Ash.Changeset.before_transaction(fn changeset ->
IO.puts("second before")
changeset
end, append?: true)
|> Ash.Changeset.after_transaction(fn changeset, result ->
IO.puts("first after")
result
end)
|> Ash.Changeset.after_transaction(fn changeset, result ->
IO.puts("second after")
result
end)
This would print:
first around: before
second around: before
first before
second before
<-- action hooks happens here
first after
second after
second around: after <-- Notice that because of the callbacks, the after of the around hooks is reversed from the before
first around: after
Warning: using this without understanding how it works can cause big problems.
You must call the callback function that is provided to your hook, and the return value must
contain the same structure that was given to you, i.e {:ok, result_of_action}
.
You can almost always get the same effect by using before_transaction
, setting some context on the changeset
and reading it out in an after_transaction
hook.
Gets a reference to a field, or the current atomic update expression of that field.
Adds multiple atomic changes to the changeset
See atomic_update/3
for more information.
@spec atomic_update(t(), atom(), {:atomic, Ash.Expr.t()} | Ash.Expr.t()) :: t()
Adds an atomic change to the changeset.
Atomic changes are applied by the data layer, and as such have guarantees that are not given by changes that are based on looking at the previous value and updating it. Here is an example of a change that is not safe to do concurrently:
change fn changeset, _ ->
Ash.Changeset.change_attribute(changeset, :score, changeset.data.score + 1)
end
If two processes run this concurrently, they will both read the same value of score
, and
set the new score to the same value. This means that one of the increments will be lost.
If you were to instead do this using atomic_update
, you would get the correct result:
Ash.Changeset.atomic_update(changeset, :score, [expr(score + 1)])
There are drawbacks/things to consider, however. The first is that atomic update results are not known until after the action is run. The following functional validation would not be able to enforce the score being less than 10, because the atomic happens after the validation.
validate fn changeset, _ ->
if Ash.Changeset.get_attribute(changeset, :score) < 10 do
:ok
else
{:error, field: :score, message: "must be less than 10"}
end
end
If you want to use atomic updates, it is suggested to write module-based validations & changes, and implement the appropriate atomic callbacks on those modules. All builtin validations and changes implement these callbacks in addition to the standard callbacks. Validations will only be run atomically when the entire action is being run atomically or if one of the relevant fields is being updated atomically.
Checks if an attribute is not nil, either in the original data, or that it is not being changed to a nil
value if it is changing.
This also accounts for the accessing_from
context that is set when using manage_relationship
, so it is aware that a particular value
will be set by manage_relationship
even if it isn't currently being set.
@spec before_action( changeset :: t(), fun :: before_action_fun(), opts :: Keyword.t() ) :: t()
Adds a before_action hook to the changeset.
Provide the option prepend?: true
to place the hook before all
other hooks instead of after.
@spec before_transaction( t(), before_transaction_fun(), Keyword.t() ) :: t()
Adds a before_transaction hook to the changeset.
Provide the option prepend?: true
to place the hook before all
other hooks instead of after.
Adds a change to the changeset, unless the value matches the existing value.
Calls change_attribute/3
for each key/value pair provided.
The same as change_attribute
, but annotates that the attribute is currently holding a default value.
This information can be used in changes to see if a value was explicitly set or if it was set by being the default.
Additionally, this is used in upsert
actions to not overwrite existing values with the default.
Change an attribute only if is not currently being changed
Change an attribute if is not currently being changed, by calling the provided function.
Use this if you want to only perform some expensive calculation for an attribute value only if there isn't already a change for that attribute.
Returns true if an attribute exists in the changes
Returns true if any attributes on the resource are being changed.
Returns true if a relationship exists in the changes
Clears an attribute or relationship change off of the changeset.
Remove an argument from the changeset
Ensure the the specified attributes are nil
in the changeset results.
Ensures that the given attributes are selected.
The first call to select/2
will limit the fields to only the provided fields.
Use ensure_selected/2
to say "select this field (or these fields) without deselecting anything else".
See select/2
for more.
Turns the special case {:replace, fields}, :replace_all and {:replace_all_except, fields} upsert_fields options into a list of fields
Fetches the value of an argument provided to the changeset or :error
.
Gets the value of an argument provided to the changeset, falling back to Ash.Changeset.fetch_change/2
if nothing was provided.
Gets the new value for an attribute, or :error
if it is not being changed.
@spec filter(t(), Ash.Expr.t()) :: t()
Adds a filter for a record being updated or destroyed.
Used by optimistic locking. See Ash.Resource.Change.Builtins.optimistic_lock/1
for more.
Constructs a changeset for a given action, and validates it.
Calls for_create/4
, for_update/4
or for_destroy/4
based on the type of action passed in.
See those functions for more explanation.
Constructs a changeset for a given create action, and validates it.
Anything that is modified prior to for_create/4
is validated against the rules of the action, while anything after it is not.
This runs any change
s contained on your action. To have your logic execute only during the action, you can use after_action/2
or before_action/2
.
Multitenancy is not validated until an action is called. This allows you to avoid specifying a tenant until just before calling the domain action.
Params
params
may be attributes, relationships, or arguments. You can safely pass user/form input directly into this function.
Only public attributes and relationships are supported. If you want to change private attributes as well, see the
Customization section below. params
are stored directly as given in the params
field of the changeset, which is used
Opts
:require?
(boolean/0
) - If set tofalse
, values are only required when the action is run (instead of immediately). The default value isfalse
.:actor
(term/0
) - set the actor, which can be used in anyAsh.Resource.Change
s configured on the action. (in thecontext
argument):authorize?
(term/0
) - set authorize?, which can be used in anyAsh.Resource.Change
s configured on the action. (in thecontext
argument):tracer
(one or a list of module that adoptsAsh.Tracer
) - A tracer to use. Will be carried over to the action. For more information seeAsh.Tracer
.:tenant
(value that implements theAsh.ToTenant
protocol) - set the tenant on the changeset:skip_unknown_inputs
- A list of inputs that, if provided, will be ignored if they are not recognized by the action. Use:*
to indicate all unknown keys.:context
(map/0
) - Context to set on the query, changeset, or input
Customization
A changeset can be provided as the first argument, instead of a resource, to allow setting specific attributes ahead of time.
For example:
MyResource
|> Ash.Changeset.new()
|> Ash.Changeset.change_attribute(:foo, 1)
|> Ash.Changeset.for_create(:create, ...opts)
Once a changeset has been validated by for_create/4
(or for_update/4
), it isn't validated again in the action.
New changes added are validated individually, though. This allows you to create a changeset according
to a given action, and then add custom changes if necessary.
What does this function do?
The following steps are run when calling Ash.Changeset.for_create/4
.
Cast input params | This is any arguments in addition to any accepted attributes
- Set argument defaults
- Require any missing arguments
- Validate all provided attributes are accepted
- Require any accepted attributes that are
allow_nil?
false - Set any default values for attributes
- Run action changes & validations
- Run validations, or add them in
before_action
hooks if usingAsh.Resource.Dsl.actions.create.validate.before_action?
. Any global validations are skipped if the action hasskip_global_validations?
set totrue
.
Constructs a changeset for a given destroy action, and validates it.
Opts
:actor
- set the actor, which can be used in anyAsh.Resource.Change
s configured on the action. (in thecontext
argument):tenant
- set the tenant on the changeset
Anything that is modified prior to for_destroy/4
is validated against the rules of the action, while anything after it is not.
Once a changeset has been validated by for_destroy/4
, it isn't validated again in the action.
New changes added are validated individually, though. This allows you to create a changeset according
to a given action, and then add custom changes if necessary.
What does this function do?
The following steps are run when calling Ash.Changeset.for_destroy/4
.
Cast input params | This is any arguments in addition to any accepted attributes
- Set argument defaults
- Require any missing arguments
- Validate all provided attributes are accepted
- Require any accepted attributes that are
allow_nil?
false - Set any default values for attributes
- Run action changes & validations
- Run validations, or add them in
before_action
hooks if usingAsh.Resource.Dsl.actions.destroy.validate.before_action?
. Any global validations are skipped if the action hasskip_global_validations?
set totrue
.
Constructs a changeset for a given update action, and validates it.
Anything that is modified prior to for_update/4
is validated against the rules of the action, while anything after it is not.
What does this function do?
The following steps are run when calling Ash.Changeset.for_update/4
.
Cast input params | This is any arguments in addition to any accepted attributes
- Set argument defaults
- Require any missing arguments
- Validate all provided attributes are accepted
- Require any accepted attributes that are
allow_nil?
false - Set any default values for attributes
- Run action changes & validations
- Run validations, or add them in
before_action
hooks if usingAsh.Resource.Dsl.actions.update.validate.before_action?
. Any global validations are skipped if the action hasskip_global_validations?
set totrue
.
Changes an attribute even if it isn't writable
Calls force_change_attribute/3
for each key/value pair provided.
Force change an attribute if it is not currently being changed.
See change_new_attribute/3
for more.
Force change an attribute if it is not currently being changed, by calling the provided function.
See change_new_attribute_lazy/3
for more.
Add an argument to the changeset, which will be provided to the action.
Does not show a warning when used in before/after action hooks.
Merge a map of arguments to the arguments list.
Does not show a warning when used in before/after action hooks.
@spec fully_atomic_changeset( resource :: Ash.Resource.t(), action :: atom() | Ash.Resource.Actions.action(), params :: map(), opts :: Keyword.t() ) :: t() | {:not_atomic, String.t()}
Gets the value of an argument provided to the changeset.
Gets the value of an argument provided to the changeset, falling back to Ash.Changeset.get_attribute/2
if nothing was provided.
Gets the changing value or the original value of an attribute.
Gets the original value for an attribute
@spec handle_errors( t(), (t(), error :: term() -> :ignore | t() | (error :: term()) | {error :: term(), t()}) | {module(), atom(), [term()]} ) :: t()
Sets a custom error handler on the changeset.
The error handler should be a two argument function or an mfa, in which case the first two arguments will be set to the changeset and the error, w/ the supplied arguments following those.
Any errors generated are passed to handle_errors
, which can return any of the following:
:ignore
- the error is discarded, and the changeset is not marked as invalidchangeset
- a new (or the same) changeset. The error is not added (you'll want to add an error yourself), but the changeset is marked as invalid.{changeset, error}
- a new (or the same) error and changeset. The error is added to the changeset, and the changeset is marked as invalid.anything_else
- is treated as a new, transformed version of the error. The result is added as an error to the changeset, and the changeset is marked as invalid.
handle_params(changeset, action, params, handle_params_opts \\ [])
View SourceCalls the provided load statement on the result of the action at the very end of the action.
Returns true if the field/relationship or path to field/relationship is being loaded.
It accepts an atom or a list of atoms, which is treated for as a "path", i.e:
Resource |> Ash.Changeset.load(friends: [enemies: [:score]]) |> Ash.Changeset.loading?([:friends, :enemies, :score])
iex> true
Resource |> Ash.Changeset.load(friends: [enemies: [:score]]) |> Ash.Changeset.loading?([:friends, :score])
iex> false
Resource |> Ash.Changeset.load(friends: [enemies: [:score]]) |> Ash.Changeset.loading?(:friends)
iex> true
Manages the related records by creating, updating, or destroying them as necessary.
Keep in mind that the default values for all on_*
are :ignore
, meaning nothing will happen
unless you provide instructions.
The input provided to manage_relationship
should be a map, in the case of to_one relationships, or a list of maps
in the case of to_many relationships. The following steps are followed for each input provided:
- The input is checked against the currently related records to find any matches. The primary key and unique identities are used to find matches.
- For any input that had a match in the current relationship, the
:on_match
behavior is triggered - For any input that does not have a match:
- if there is
on_lookup
behavior:- we try to find the record in the data layer.
- if the record is found, the on_lookup behavior is triggered
- if the record is not found, the
on_no_match
behavior is triggered
- if there is no
on_lookup
behavior:- the
on_no_match
behavior is triggered
- the
- if there is
- finally, for any records present in the current relationship that had no match in the input, the
on_missing
behavior is triggered
Options
:type
- If thetype
is specified, the default values of each option is modified to match thattype
of operation.
This allows for specifying certain operations much more succinctly. The defaults that are modified are listed below::append_and_remove
[ on_lookup: :relate, on_no_match: :error, on_match: :ignore, on_missing: :unrelate ]
:append
[ on_lookup: :relate, on_no_match: :error, on_match: :ignore, on_missing: :ignore ]
:remove
[ on_no_match: :error, on_match: :unrelate, on_missing: :ignore ]
:direct_control
[ on_lookup: :ignore, on_no_match: :create, on_match: :update, on_missing: :destroy ]
:create
Valid values are :append_and_remove, :append, :remove, :direct_control, :create[ on_no_match: :create, on_match: :ignore ]
:authorize?
(boolean/0
) - Authorize reads and changes to the destination records, if the primary change is being authorized as well. The default value istrue
.:eager_validate_with
(atom/0
) - Validates that any referenced entities exist before the action is being performed, using the provided domain for the read. The default value isfalse
.:on_no_match
(term/0
) - Instructions for handling records where no matching record existed in the relationship.:ignore
(default) - those inputs are ignored:match
- Forhas_one
andbelongs_to
only, any input is treated as a match for an existing value. Forhas_many
andmany_to_many
, this is the same as:ignore
.:create
- the records are created using the destination's primary create action{:create, :action_name}
- the records are created using the specified action on the destination resource{:create, :action_name, :join_table_action_name, [:list, :of, :join_table, :params]}
- Same as{:create, :action_name}
but takes the list of params specified out and applies them when creating the join record, with the provided join_table_action_name.:error
- an error is returned indicating that a record would have been created- If
on_lookup
is set, and the data contained a primary key or identity, then the error is aNotFound
error - Otherwise, an
InvalidRelationship
error is returned The default value is:ignore
.
- If
:value_is_key
(atom/0
) - Configures what key to use when a single value is provided.
This is useful when you use things like a list of strings i.efriend_emails
to manage the relationship, instead of a list of maps.
By default, we assume it is the primary key of the destination resource, unless it is a composite primary key.:identity_priority
(list ofatom/0
) - The list, in priority order, of identities to use when looking up records foron_lookup
, and matching records withon_match
.
Use:_primary_key
to prioritize checking a match with the primary key. All identities, along with:_primary_key
are checked regardless, this only allows ensuring that some are checked first. Defaults to the list provided byuse_identities
, so you typically won't need this option.:use_identities
(list ofatom/0
) - A list of identities that may be used to look up and compare records. Use:_primary_key
to include the primary key. By default, only[:_primary_key]
is used.:on_lookup
(term/0
) - Before creating a record (because no match was found in the relationship), the record can be looked up and related.:ignore
(default) - Does not look for existing entries (matches in the current relationship are still considered updates):relate
- Same as calling{:relate, primary_action_name}
{:relate, :action_name}
- the records are looked up by primary key/the first identity that is found (using the primary read action), and related. The action should be:many_to_many
- a create action on the join resourcehas_many
- an update action on the destination resourcehas_one
- an update action on the destination resourcebelongs_to
- an update action on the source resource
{:relate, :action_name, :read_action_name}
- Same as the above, but customizes the read action called to search for matches.:relate_and_update
- Same as:relate
, but the remaining parameters from the lookup are passed into the action that is used to change the relationship key{:relate_and_update, :action_name}
- Same as the above, but customizes the action used. The action should be:many_to_many
- a create action on the join resourcehas_many
- an update action on the destination resourcehas_one
- an update action on the destination resourcebelongs_to
- an update action on the source resource
{:relate_and_update, :action_name, :read_action_name}
- Same as the above, but customizes the read action called to search for matches.{:relate_and_update, :action_name, :read_action_name, [:list, :of, :join_table, :params]}
- Same as the above, but uses the provided list of parameters when creating the join row (only relevant for many to many relationships). Use:*
to only update the join record, and pass all parameters to its action The default value is:ignore
.
:on_match
(term/0
) - Instructions for handling records where a matching record existed in the relationship already.:ignore
(default) - those inputs are ignored:update
- the record is updated using the destination's primary update action{:update, :action_name}
- the record is updated using the specified action on the destination resource{:update, :action_name, :join_table_action_name, [:list, :of, :params]}
- Same as{:update, :action_name}
but takes the list of params specified out and applies them as an update to the join record (only valid for many to many):update_join
- update only the join record (only valid for many to many){:update_join, :join_table_action_name}
- use the specified update action on a join resource{:update_join, :join_table_action_name, [:list, :of, :params]}
- pass specified params from input into a join resource update action{:destroy, :action_name}
- the record is destroyed using the specified action on the destination resource. The action should be:many_to_many
- a destroy action on the join recordhas_many
- a destroy action on the destination resourcehas_one
- a destroy action on the destination resourcebelongs_to
- a destroy action on the destination resource
:error
- an error is returned indicating that a record would have been updated:no_match
- follows theon_no_match
instructions with these records:missing
- follows theon_missing
instructions with these records:unrelate
- the related item is not destroyed, but the data is "unrelated", making this behave likeremove_from_relationship/3
. The action should be:many_to_many
- the join resource row is destroyedhas_many
- thedestination_attribute
(on the related record) is set tonil
has_one
- thedestination_attribute
(on the related record) is set tonil
belongs_to
- thesource_attribute
(on this record) is set tonil
{:unrelate, :action_name}
- the record is unrelated using the provided update action. The action should be:many_to_many
- a destroy action on the join resourcehas_many
- an update action on the destination resourcehas_one
- an update action on the destination resourcebelongs_to
- an update action on the source resource The default value is:ignore
.
:on_missing
(term/0
) - Instructions for handling records that existed in the current relationship but not in the input.:ignore
(default) - those inputs are ignored:destroy
- the record is destroyed using the destination's primary destroy action{:destroy, :action_name}
- the record is destroyed using the specified action on the destination resource{:destroy, :action_name, :join_resource_action_name}
- the record is destroyed using the specified action on the destination resource, but first the join resource is destroyed with its specified action:error
- an error is returned indicating that a record would have been updated:unrelate
- the related item is not destroyed, but the data is "unrelated", making this behave likeremove_from_relationship/3
. The action should be:many_to_many
- the join resource row is destroyedhas_many
- thedestination_attribute
(on the related record) is set tonil
has_one
- thedestination_attribute
(on the related record) is set tonil
belongs_to
- thesource_attribute
(on this record) is set tonil
{:unrelate, :action_name}
- the record is unrelated using the provided update action. The action should be:many_to_many
- a destroy action on the join resourcehas_many
- an update action on the destination resourcehas_one
- an update action on the destination resourcebelongs_to
- an update action on the source resource The default value is:ignore
.
:error_path
(term/0
) - By default, errors added to the changeset will use the path[:relationship_name]
, or[:relationship_name, <index>]
. If you want to modify this path, you can specifyerror_path
, e.g if had achange
on an action that takes an argument and uses that argument data to callmanage_relationship
, you may want any generated errors to appear under the name of that argument, so you could specifyerror_path: :argument_name
when callingmanage_relationship
.:join_keys
(list ofatom/0
) - For many to many relationships specifies the parameters to pick from the input and pass into a join resource action. Applicable in cases likeon_no_match: :create
,on_match: :update
andon_lookup: :relate
. Can be overwritten by a full form instruction tuple which contains join parameters at the end.:meta
(term/0
) - Freeform data that will be retained along with the options, which can be used to track/manage the changes that are added to therelationships
key.:ignore?
(term/0
) - This tells Ash to ignore the provided inputs when actually running the action. This can be useful for building up a set of instructions that you intend to handle manually. The default value isfalse
.
Each call to this function adds new records that will be handled according to their options. For example, if you tracked "tags to add" and "tags to remove" in separate fields, you could input them like so:
changeset
|> Ash.Changeset.manage_relationship(
:tags,
[%{name: "backend"}],
on_lookup: :relate, #relate that tag if it exists in the database
on_no_match: :error # error if a tag with that name doesn't exist
)
|> Ash.Changeset.manage_relationship(
:tags,
[%{name: "frontend"}],
on_no_match: :error, # error if a tag with that name doesn't exist in the relationship
on_match: :unrelate # if a tag with that name is related, unrelate it
)
When calling this multiple times with the on_missing
option set, the list of records that are considered missing are checked
after each set of inputs is processed. For example, if you manage the relationship once with on_missing: :unrelate
, the records
missing from your input will be removed, and then your next call to manage_relationship
will be resolved (with those records unrelated).
For this reason, it is suggested that you don't call this function multiple times with an on_missing
instruction, as you may be
surprised by the result.
If you want the input to update existing entities, you need to ensure that the primary key (or unique identity) is provided as part of the input. See the example below:
changeset
|> Ash.Changeset.manage_relationship(
:comments,
[%{rating: 10, contents: "foo"}],
on_no_match: {:create, :create_action},
on_missing: :ignore
)
|> Ash.Changeset.manage_relationship(
:comments,
[%{id: 10, rating: 10, contents: "foo"}],
on_match: {:update, :update_action},
on_no_match: {:create, :create_action})
This is a simple way to manage a relationship. If you need custom behavior, you can customize the action that is called, which allows you to add arguments/changes. However, at some point you may want to forego this function and make the changes yourself. For example:
input = [%{id: 10, rating: 10, contents: "foo"}]
changeset
|> Ash.Changeset.after_action(fn _changeset, result ->
# An example of updating comments based on a result of other changes
for comment <- input do
comment = Ash.get(Comment, comment.id)
comment
|> Map.update(:rating, 0, &(&1 * result.rating_weight))
|> Ash.update!()
end
{:ok, result}
end)
Using records as input
Records can be supplied as the input values. If you do:
- if it would be looked up due to
on_lookup
, the record is used as-is - if it would be created due to
on_no_match
, the record is used as-is - Instead of specifying
join_keys
, those keys must go in__metadata__.join_keys
. Ifjoin_keys
is specified in the options, it is ignored.
For example:
post1 =
changeset
|> Ash.create!()
|> Ash.Resource.put_metadata(:join_keys, %{type: "a"})
post1 =
changeset2
|> Ash.create!()
|> Ash.Resource.put_metadata(:join_keys, %{type: "b"})
author = Ash.create!(author_changeset)
Ash.Changeset.manage_relationship(
author,
:posts,
[post1, post2],
on_lookup: :relate
)
@spec manage_relationship_opts(manage_relationship_type()) :: Keyword.t()
@spec new(Ash.Resource.t() | Ash.Resource.record()) :: t()
Returns a new changeset over a resource.
Warning: You almost always want to use for_action
or for_create
, etc. over this function if possible.
You can use this to start a changeset and make changes prior to calling for_action
. This is not typically
necessary, but can be useful as an escape hatch. For example:
Resource
|> Ash.Changeset.new()
|> Ash.Changeset.change_attribute(:name, "foobar")
|> Ash.Changeset.for_action(...)
Checks if an argument is not nil or an attribute is not nil, either in the original data, or that it is not being changed to a nil
value if it is changing.
This also accounts for the accessing_from
context that is set when using manage_relationship
, so it is aware that a particular value
will be set by manage_relationship
even if it isn't currently being set.
Puts a key/value in the changeset context that can be used later.
Do not use the private
key in your custom context, as that is reserved for internal use.
Ensure that only the specified attributes are present in the results.
The first call to select/2
will replace the default behavior of selecting
all attributes. Subsequent calls to select/2
will combine the provided
fields unless the replace?
option is provided with a value of true
.
If a field has been deselected, selecting it again will override that (because a single list of fields is tracked for selection)
Primary key attributes always selected and cannot be deselected.
When attempting to load a relationship (or manage it with Ash.Changeset.manage_relationship/3
),
if the source field is not selected on the query/provided data an error will be produced. If loading
a relationship with a query, an error is produced if the query does not select the destination field
of the relationship.
Datalayers currently are not notified of the select
for a changeset(unlike queries), and creates/updates select all fields when they are performed.
A select provided on a changeset sets the unselected fields to nil
before returning the result.
Use ensure_selected/2
if you wish to make sure a field has been selected, without deselecting any other fields.
Add an argument to the changeset, which will be provided to the action.
Merge a map of arguments to the arguments list.
Deep merges the provided map into the changeset context that can be used later.
Do not use the private
key in your custom context, as that is reserved for internal use.
Set the result of the action. This will prevent running the underlying datalayer behavior
@spec set_tenant(t(), Ash.ToTenant.t()) :: t()
@spec timeout(t(), nil | pos_integer(), nil | pos_integer()) :: t()
Updates an existing attribute change by applying a function to it.
This is useful for applying some kind of normalization to the attribute.
Ash.Changeset.update_change(changeset, :serial, &String.downcase/1)
The update function gets called with the value already cast to the correct type.
changeset
|> Ash.Changeset.change_attribute(:integer_attribute, "3")
|> Ash.Changeset.update_change(:integer_attribute, fn x -> x + 1 end)
Invalid value handling
If update_change
is called with a changeset that has not been validated yet, the update
function must handle potentially invalid and nil
values.
To only deal with valid values, you can call update_change
in a before_action
hook.
@spec with_hooks( t(), (t() -> {:ok, term(), %{notifications: [Ash.Notifier.Notification.t()]}} | {:error, term()}), Keyword.t() ) :: {:ok, term(), t(), %{notifications: [Ash.Notifier.Notification.t()]}} | {:error, term()}
Wraps a function in the before/after action hooks of a changeset.
The function takes a changeset and if it returns
{:ok, result}
, the result will be passed through the after
action hooks.