ash/documentation/topics/actions/create-actions.md
Zach Daniel d2ea5bb108 docs: rewrite action docs
improvement: synthesize attributes from atomics for better notifications
2024-04-07 08:02:10 -04:00

97 lines
4.9 KiB
Markdown

# Create Actions
Create actions are used to create new records in the data layer. For example:
```elixir
# 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:
```elixir
Ticket
|> Ash.Changeset.for_create(:open, %{title: "Need help!"})
|> Ash.create!()
```
See the [Code Interface guide](documentation/topics/code-interface.md) for creating an interface to call the action more elegantly, like so:
```elixir
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:
```elixir
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 implementing `batch_change/3`, `before_batch/3` and `after_batch/3`. If you implement `batch_change/3`, the `change` function will no longer be called, and you should swap any behavior implemented with `before_action` and `after_action` hooks to logic in the `before_batch` and `after_batch` callbacks.
- Actions that reference arguments in changes, i.e `change set_attribute(:attr, ^arg(:arg))` will prevent us from using the `batch_change/3` behavior. This is usually not a problem, for instance that change is lightweight and would not benefit from being optimized with `batch_change/3`
- If your action uses `after_action` hooks, or has `after_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 using `return_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:
```elixir
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:
> ```elixir
> [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` or `Enum.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` and `around_transaction` hooks are called (`Ash.Changeset.before_transaction/2`). Keep in mind, any validations that are marked as `before_action? true` (or all global validations if your action has `delay_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)