Update belongs_to record on create through managed association

Hey hey,

I’ve just been trying to implement something that I had thought reasonable but am getting errors back and figured I’d see if it’s expected behavior or if something’s amiss.

I have a few resources as part of a ledger that have the following relevant actions:

# transaction.ex

    create :create_with_entries do
      argument :entries, {:array, :map} do
        allow_nil? false
        constraints min_length: 1
      end

      change set_attribute(:id, &Ash.UUIDv7.generate/0)
      change set_attribute(:time, &DateTime.utc_now/0)

      change fn changeset, _ ->
        entries_arg = Ash.Changeset.get_argument(changeset, :entries)

        transaction_id = Ash.Changeset.get_attribute(changeset, :id)
        transaction_time = Ash.Changeset.get_attribute(changeset, :time)

        entries =
          entries_arg
          |> Enum.map(fn entry ->
            entry
            |> Map.put(:transaction_id, transaction_id)
            |> Map.put(:time, transaction_time)
          end)

        changeset
        |> Ash.Changeset.manage_relationship(:entries, entries,
          on_no_match: {:create, :create_and_update_account_balance}
        )
      end

      # This for some reason wraps the action in a transaction that it otherwise wouldn't
      change after_action(fn _changeset, result, _context ->
               {:ok, result}
             end)
    end

# entry.ex

    create :create_and_update_account_balance do
      accept [:transaction_id, :account_id, :time, :amount]

      change fn changeset, _context ->
        changeset
        |> Ash.Changeset.manage_relationship(
          :account,
          %{
            id: Ash.Changeset.get_attribute(changeset, :account_id),
            amount: Ash.Changeset.get_attribute(changeset, :amount)
          },
          on_match: {:update, :update_balance},
          on_no_match: :error
        )
      end
    end

# account.ex

    update :update_balance do
      argument :amount, :decimal do
        allow_nil? false
      end

      change atomic_update(
               :balance,
               expr(
                 if ^arg(:amount) > 0 do
                   balance + ^arg(:amount)
                 else
                   if balance >= -(^arg(:amount)) do
                     balance + ^arg(:amount)
                   else
                     error(
                       Ash.Error.Changes.InvalidArgument,
                       %{
                         field: :amount,
                         message: "Insufficient balance"
                       }
                     )
                   end
                 end
               )
             )
    end

On running the following command I get an error:

# iex

Transaction
|> Ash.Changeset.for_create(:create_with_entries, %{entries: entries})
|> Ash.create!()

* Invalid value provided for account: changes would create a new related record.

I can bypass this issue by instead using an after_action:


# entry.ex

    create :create_and_update_account_balance do
      accept [:transaction_id, :account_id, :time, :amount]

      change after_action(fn _changeset, result, _context ->
               account =
                 %Account{id: result.account_id}
                 |> Ash.Changeset.for_update(:update_balance, %{amount: result.amount})
                 |> Ash.update!()

               result = Map.put(result, :account, account)

               {:ok, result}
             end)
    end

# iex

Transaction
|> Ash.Changeset.for_create(:create_with_entries, %{entries: entries})
|> Ash.create!()

[debug] QUERY OK source="transactions" db=1.7ms
[debug] QUERY OK source="entries" db=4.5ms
[debug] QUERY OK source="accounts" db=12.9ms

Also, in doing some digging around I was able to get the manage_relationship to work via an update action but this led to additional queries and doesn’t solve the issue:


# entry

    update :update_and_update_account_balance do
      require_atomic? false

      argument :account, :map, allow_nil?: false

      change manage_relationship(:account, :account,
               on_match: {:update, :update_balance},
               on_no_match: :error
             )
    end

# iex

entry
|> Ash.Changeset.for_update(:update_and_update_account_balance, %{account: %{id: entry.account_id, amount: 1}})
|> Ash.update!

[debug] QUERY OK source="entries" db=8.0ms
[debug] QUERY OK source="accounts" db=3.3ms
[debug] QUERY OK source="entries" db=1.5ms
[debug] QUERY OK source="accounts" db=0.5ms
[debug] QUERY OK source="accounts" db=1.4ms

Few questions from this:

  1. Is it at all possible to update a managed relationship via a create action?
  2. Even if possible, is there a different way of doing it that’s preferred, e.g. via an after_action?
  3. [side] why is the :create_with_entries action not being run in a transaction unless providing an after_action

Note: transactions and entries are timescaledb hypertables and entries has no primary key but I added one and it didn’t fix anything (except for enabling the update action)

:thinking: I don’t see why that wouldn’t work. Does loading account work in the same context?

Hey, thanks for the prompt reply

I’m guessing you mean:

    create :create_and_update_account_balance do
      accept [:transaction_id, :account_id, :time, :amount]

      change load(:account)
    end

Which does work