improvement: synthesize attributes from atomics for better notifications
4.9 KiB
Create Actions
Create actions are used to create new records in the data layer. For example:
# on a ticket resource
create :open do
accept [:title]
change set_attribute(status: :open)
end
Here we have a create action called :open
that allows setting the title
, and sets the status
to :open
. It could be called like so:
Ticket
|> Ash.Changeset.for_create(:open, %{title: "Need help!"})
|> Ash.create!()
See the Code Interface guide for creating an interface to call the action more elegantly, like so:
Support.open_ticket!("Need help!")
Bulk creates
Bulk creates take a list or stream of inputs for a given action, and batches calls to the underlying data layer.
Given our example above, you could call Ash.bulk_create
like so:
Ash.bulk_create([%{title: "Foo"}, %{title: "Bar"}], Ticket, :open)
Check the docs! {: .warning}
Make sure to thoroughly read and understand the documentation in
Ash.bulk_create/4
before using. Read each option and note the default values. By default, bulk creates don't return records or errors, and don't emit notifications.
Performance
Generally speaking, all regular Ash create actions are compatible (or can be made to be compatible) with bulk create actions. However, there are some important considerations.
-
Ash.Resource.Change
modules can be optimized for bulk actions by implementingbatch_change/3
,before_batch/3
andafter_batch/3
. If you implementbatch_change/3
, thechange
function will no longer be called, and you should swap any behavior implemented withbefore_action
andafter_action
hooks to logic in thebefore_batch
andafter_batch
callbacks. -
Actions that reference arguments in changes, i.e
change set_attribute(:attr, ^arg(:arg))
will prevent us from using thebatch_change/3
behavior. This is usually not a problem, for instance that change is lightweight and would not benefit from being optimized withbatch_change/3
-
If your action uses
after_action
hooks, or hasafter_batch/3
logic defined for any of its changes, then we must ask the data layer to return the records it inserted. Again, this is not generally a problem because we throw away the results of each batch by default. If you are usingreturn_records?: true
then you are already requesting all of the results anyway.
Returning a Stream
Returning a stream allows you to work with a bulk action as an Elixir Stream. For example:
input_stream()
|> Ash.bulk_create(Resource, :action, return_stream?: true, return_records?: true)
|> Stream.map(fn {:ok, result} ->
# process results
{:error, error} ->
# process errors
end)
|> Enum.reduce(%{}, fn {:ok, result}, acc ->
# process results
{:error, error} ->
# process errors
end)
Be careful with streams {: .warning}
Because streams are lazily evaluated, if you were to do something like this:
[input1, input2, ...] # has 300 things in it |> Ash.bulk_create( Resource, :action, return_stream?: true, return_records?: true, batch_size: 100 # default is 100 ) |> Enum.take(150) # stream has 300, but we only take 150
What would happen is that we would insert 200 records. The stream would end after we process the first two batches of 100. Be sure you aren't using things like
Stream.take
orEnum.take
to limit the amount of things pulled from the stream, unless you actually want to limit the number of records created.
What happens when you run a create Action
When All actions are run in a transaction if the data layer supports it. You can opt out of this behavior by supplying transaction?: false
when creating the action. When an action is being run in a transaction, all steps inside of it are serialized because transactions cannot be split across processes.
- Authorization is performed on the changes
- A before action hook is added to set up belongs_to relationships that are managed. This means potentially creating/modifying the destination of the relationship, and then changing the
destination_attribute
of the relationship. before_transaction
andaround_transaction
hooks are called (Ash.Changeset.before_transaction/2
). Keep in mind, any validations that are marked asbefore_action? true
(or all global validations if your action hasdelay_global_validations? true
) will not have happened at this point.- A transaction is opened if the action is configured for it (by default they are) and the data layer supports transactions
before_action
hooks are performed in order- The main action is sent to the data layer
after_action
hooks are performed in order- Non-belongs-to relationships are managed, creating/updating/destroying related records.
- The transaction is closed, if one was opened
after_transaction
hooks are invoked with the result of the transaction (even if it was an error)