What is the "Ash way" to auto update related resources ?

Let's say that I have an invoice (postgres) resource and that it has_many lines. Let's say that I want the total_amount field of my invoice resource to be updated with the sum(amount) of all lines every time that a line is created, updated or deleted. What would be the idiomatic way to do that ? I understand that I should probably use aggregates and changes with an after_action, but I have a bit of a hard time to see exactly how to articulates all the pieces together.
9 Replies
ZachDaniel
ZachDaniel3y ago
There are a few ways to do it. Are you looking to do this as an optimization? i.e to avoid counting the total_amount each time? Or is there some other reason to persist the value?
LeJoko
LeJokoOP3y ago
I need to have it recorded. It is definitely an optimization. I tried it only with aggregates and calculations and it does work very well (I love them!), but if I continue that project, in the longer run, I will have fields that require complex calculations dependent on business rules and also need to be queryable fast. So I'd be very happy to understand how it could be done "the Ash way". Another example could be a tree of related resources where the lowest ones need to "touch" their parent recursively to mark them as "updated" I tried something like that : I made an update_total action in the invoice resource that does the job of updating the total_amount field (using an aggregate) and in the lines resource I made a change in a changes block that calls the update_caches action on the parent invoice in an after_action. It seems ok, but I've a feeling that something might be done better.
ZachDaniel
ZachDaniel3y ago
When you say using an aggregate what do you mean? You mean that you are loading the aggregate and then persisting its value? My first stab at it would likely be something like this (because Ash doesn't have updating with a query/atomic changes yet) using ecto directly
change after_action(fn changeset, result ->
query =
from row in ParentThing,
where: id == ^parent_id
MyApp.Repo.update_all(
query,
set: [total_amount: [inc: result.amount]]
)
end)
change after_action(fn changeset, result ->
query =
from row in ParentThing,
where: id == ^parent_id
MyApp.Repo.update_all(
query,
set: [total_amount: [inc: result.amount]]
)
end)
LeJoko
LeJokoOP3y ago
In my previous trials I did approximately that: In parent :
actions do
update :update_totals do
change(fn changeset, _context ->
parent = MyApi.load!(changeset.data, [:total_from_aggregate])
changeset
|> Ash.Changeset.change_attributes(%{
recorded_total: parent.total_from_aggregate,
})
end)
end
end

aggregates do
sum :total_from_aggregate, :children, :amount
end
actions do
update :update_totals do
change(fn changeset, _context ->
parent = MyApi.load!(changeset.data, [:total_from_aggregate])
changeset
|> Ash.Changeset.change_attributes(%{
recorded_total: parent.total_from_aggregate,
})
end)
end
end

aggregates do
sum :total_from_aggregate, :children, :amount
end
In child:
change after_action(fn _changeset, result ->
MyApi.load!(result, :parent).parent
|> Ash.Changeset.for_action(:update_total)
|> Parent.update!()
{:ok, result}
end)
change after_action(fn _changeset, result ->
MyApi.load!(result, :parent).parent
|> Ash.Changeset.for_action(:update_total)
|> Parent.update!()
{:ok, result}
end)
It works but feels a bit clunky. What problems do you see with it ? By the way, what result an after_action is supposed to return ? (when returning `{:ok, resource} ) the doc is not clear about how and where that result may be used.
ZachDaniel
ZachDaniel3y ago
You should return {:ok, the_thing_we_gave_you} it’s an opportunity to modify the return value in some way. So the main issue I see with that approach is that it isn’t atomic. If you had two child records created in quick succession, you could potentially end up with an incorrect total.
LeJoko
LeJokoOP3y ago
Aren't Ash update actions transactional ? If I'd lock the parent record in a before_action (or with an around_action) shouldn't that take care of the race condition ? The problem I have with your suggestion is that it works only if the total can be easily incremented by the amount. If it would require a complex calculation, it might not be so easy.
ZachDaniel
ZachDaniel3y ago
Yes, that is correct, if you lock the records in question then that is a viable strategy there is no interface in Ash to lock records though, you'll need to issue locks in ecto directly.
LeJoko
LeJokoOP3y ago
Understood. Thank you very much for your help. You answered me in the past that a MySQL connector is planned and I saw in a recent video that it is on your near future roadmap . Do you have an estimation as to when may do it ? Even a rough one... (it may be a deal breaker for me)
ZachDaniel
ZachDaniel3y ago
I don't unfortunately. Its behind bulk actions and atomics Its probably longer than a month but if you wanted to take a stab, we've decided to take a bit of a "brute force" approach to these by copying the ash_postgres data layer we can abstract out similarities later but you could probably get basic support working just changing some postgres specific stuff.

Did you find this page helpful?