How do I insert multiple records in multiple tables in a single db transaction?

How do I insert multiple records in multiple tables in a single db transaction?

You can use Ash.DataLayer — ash v2.15.8 to call multiple actions inside a transaction.

You just have to return the notifications and publish them after the transaction Notifiers — ash v2.15.8

1 Like

Thanks @barnabasJ.

With your answer, I could move one step forward.

Now, the question is: in case of failure, I call Ash.DataLayer.rollback(resource, term), right? I suppose that term is any identifier that I wanna return to specify which step could not be completed in the transaction, right? If my considerations are ok, the Ash.DataLayer.transaction will return, in case of failure, {:error, term}, right? And so does the run function.

With the setup above, when I call the action, I get the following error (only in the case of failure/rollback, in case of success, it works):

rollback []
↳ Garagem.Operacional.Estadia.run_0_generated_A916B576240B8B6C9282520D0B5CEE0D/2, at: lib/garagem/operacional/resources/estadia.ex:117
** (Ash.Error.Unknown) Unknown Error

* unknown error: :error_on_step_X
    (ash 2.15.8) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
    (stdlib 4.3.1.1) erl_eval.erl:744: :erl_eval.do_apply/7
    (stdlib 4.3.1.1) erl_eval.erl:492: :erl_eval.expr/6
    (elixir 1.15.4) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3

Could you help me out?

Are you doing this from inside of an action? Can I see the action you’re writing?

Sure.

action :faturar, :map do
  argument :estadia, :map, allow_nil?: false
  argument :terminal, :map, allow_nil?: false
  argument :parceiro_modalidade, :map, allow_nil?: false
  argument :calculo, :map, allow_nil?: false
  argument :valor_cliente_faturado, :decimal, allow_nil?: false
  argument :valor_parceiro_faturado, :decimal, allow_nil?: false
  argument :vouchers, :integer, allow_nil?: false
  argument :registrado_em, :utc_datetime_usec, allow_nil?: false

  run(fn input, _context ->
    %{"inicio_em" => first_periodo_inicio_em} = List.first(input.arguments.calculo.periodos)
    %{"fim_em" => last_periodo_fim_em} = List.last(input.arguments.calculo.periodos)

    Ash.DataLayer.transaction(EstadiaFaturamento, fn ->
      with {:ok, estadia_faturamento_1, _} <- EstadiaFaturamento.create(%{
              estadia_id: 10, # <- on purpose to fire an error
              terminal_id: input.arguments.terminal.id,
              parceiro_modalidade_id: input.arguments.parceiro_modalidade.id,
              periodo_inicio_em: first_periodo_inicio_em,
              periodo_fim_em: last_periodo_fim_em,
              valor_cliente_calculado: input.arguments.calculo.valor_cliente,
              valor_cliente_faturado: input.arguments.valor_cliente_faturado,
              valor_parceiro_calculado: input.arguments.calculo.valor_parceiro,
              valor_parceiro_faturado: input.arguments.valor_parceiro_faturado,
              registrado_em: input.arguments.registrado_em,
              vouchers: input.arguments.vouchers,
              periodos: input.arguments.calculo.periodos
            }, return_notifications?: true)
      do
        {:ok, estadia_faturamento_1}
      else
        _ ->
          Ash.DataLayer.rollback(EstadiaFaturamento, :error_on_step_X)
      end
    end)
  end)
end

You can do

action :foo, :bar do
  transaction? true

  run fn _, _ -> 
    ...
  end
end

in that case.

2 Likes

The error from the transaction will be raised if you call Resource.action! vs Resource.action

EDIT:

And if you want to return an error, you’d return an error like {:error, error} from the action. Take a look at the error handling guide for more on that front. You would potentially want to create your own Ash error using def_ash_error.

1 Like

My bad. Great. Thank you very much.