How to upsert multiple rows atomically?
How do you update multiple rows in one go, making sure each update is atomic and either all succeed or all rollback?
My use case: updating stock for an item across several warehouses.
Each update has a different increment/decrement value, and I need to make sure the final quantity never goes below 0.
Bulk update seems to set the same value for all rows, but what I need is per-row updates that happen safely based on the current db value, all inside a transaction.
Also, what’s the best way to handle cases where the row doesn’t exist yet? I’m guessing I’d need to use some form of upsert to insert the initial value and still keep everything atomic.
9 Replies
A bulk create with upsert is what you're looking for 👍
Hi Zach. Thanks for the reply in such busy moments.
I'm trying this:
Following the example from https://hexdocs.pm/ash/create-actions.html#atomic-updates.
My goal is the typical upsert, insert if doesn't exist, update if it does (based on current db quantity, not in-memory data to avoid race conditions)
The action above is working, but requires me to remove my
greater_than: -1
constraint
I get the following when passing a negative delta (which is valid for updates)
Indeed I want to validate when creating a new stock that the value is not negative, but this approach is rejecting my delta right away, even if the pk I'm passing indicates the stock already exists, so no need to create one, what could I do in this scenario?attribute constraints don't apply only on create
they apply always
Mm what can I do then? Need to validate the quantity does not go below 0, allow negative delta, and apply them only if the quantity already exists and q - delta is not less than 0. If the stock for such product doesn't even exist, I should reject such delta right away
Tried this:
But it rejects negative deltas immediately, because of this line I guess:
Yeah I think the difficulty here is that an upsert treats these things as possibly being able to create a record
and it sounds like you're doing something that could theoretically create something with a negative value
I'm actually not sure if we can reasonably support it currently. Basically what you need is something like atomic_update but that is only evaluated on create
To allow for doing something like
atomic_set(:field, expr(if(^arg(:delta) <= -1, error(...), ^arg(:delta))))
but we don't support it currently. So you will probably need to write a generic action and call Ecto yourself for thisThank you for the clarification. And yeah, that is the approach I'm currently taking and I've managed to make it work:
My doubt is if the actions called within this generic action will run in a single transaction?
You can set
transaction? true
on the action and then they will
And consider taking some kind of lock to avoid race conditionsThank you for the warning, implemented a change using the builtin helper
get_and_lock_for_update/1
for the update action. Please let me know if there's anything else I should consider.
Another question I have: how should I return errors in this scenario? As you can see, I used bang functions mainly to make sure everything was working. Now that it does, I’d like to improve the implementation by returning meaningful errors that AshPhoenix can properly map to Inertia errors using the assign_errors/1
helper. What would you recommend?You'd use
Ash.Changeset.add_error
Using bang functions is fine if you just want to abort and have the transaction rolled back