zac
Best approach: update affecting other resources (multiple changesets from one action)
I’m trying to figure out how to do this, and have come up with something that may be the completely wrong approach. ![]()
Imagine we have a resource that represents a column in a table. Each column has a number so I can easily represent it; just sort :asc in order, and draw from left-to-right.
Now, someone wants to change the order of the columns. I think there are two (fundamental) approach here: 1) I could read all the columns into memory, reorder them at the UI level, and then update the entire set. Maybe that’s the best option. But I started down the path of writing a update :set_column_number that looks like this:
NOTE this is all basically pseudocode, as I haven’t got it working.
update :set_column_number do
argument :new_number, :integer
change Api.Column.Preparations.SetNumber
end
My thinking is that the preparation could reorder the necessary columns. This is where I’m heading with it:
defmodule Api.Column.Preparations.SetNumber do
def change(changeset, _, _) do
current_number = Ash.Changeset.get_attribute(changeset, :number)
new_number = Ash.Changeset.get_argument(changeset, :new_number)
# change the target column to it's new position
Ash.Changeset.change_attribute(changeset, :column, new_number)
# collect any other affected columns that we need to re-order
other_columns = Api.Column
|> Ash.Query.filter(number >= ^new_column and number != ^number)
|> Api.read!()
# re-order them (well, create a changeset for the lot of them anyhow)
all_changes = for column <- other_columns do
column
|> Ash.Changeset.for_update(:update, [number: number + 1])
end
# hand-wavy magic about combining the changesets and "it just works"
# hint: (it doesn't, I get back an list of changesets and... not sure what next)
all_changes ++ changeset
end
In theory it seems like it should work. Main questions:
- Is there a better way to do it (using Ash, versus in the UI layer)
- And if this isn’t a really bad approach, how to fix up the last little bit (after combining the changesets)?
Marked As Solved
zachdaniel
Hello! At the moment, Ash does not have bulk updates, so this is in fact the best way to do this if you want to use only Ash (i.e iterating each and updating its position). Ash will have bulk updates (updates with a query that execute as a single update statement), but until then, I’d suggest using Ecto directly.
defmodule Api.Column.Preparations.SetNumber do
def change(changeset, _, _) do
id = changeset.data.id
current_number = Ash.Changeset.get_attribute(changeset, :number)
new_number = Ash.Changeset.get_argument(changeset, :new_number)
# change the target column to it's new position
Ash.Changeset.change_attribute(changeset, :column, new_number)
# use a before action hook
Ash.Changeset.before_action(changeset, fn changeset ->
{:ok, bumping_forward_query} =
Api.Column
|> Ash.Query.filter(number >= ^new_column and id != ^id)
|> Ash.Query.data_layer_query()
{:ok, bumping_backwards_query} =
Api.Column
|> Ash.Query.filter(number <= ^new_number and number >= current_number and id != ^id)
|> Ash.Query.data_layer_query()
YourRepo.update(bumping_forward_query, inc: [number: 1])
YourRepo.update(bumping_backwards_query, dec: [number: -1])
changeset
end)
end
You can also simplify by removing the argument and just detecting the column changing
defmodule Api.Column.Preparations.SetNumber do
def change(changeset, _, _) do
if Ash.Changeset.changing_attribute?(changeset, :number) do
id = changeset.data.id
current_number = changeset.data.number
new_number = Ash.Changeset.get_attribute(changeset, :number)
# use a before action hook
Ash.Changeset.before_action(changeset, fn changeset ->
{:ok, bumping_forward_query} =
Api.Column
|> Ash.Query.filter(number >= ^new_column and id != ^id)
|> Ash.Query.data_layer_query()
{:ok, bumping_backwards_query} =
Api.Column
|> Ash.Query.filter(number <= ^new_number and number >= current_number and id != ^id)
|> Ash.Query.data_layer_query()
YourRepo.update(bumping_forward_query, inc: [number: 1])
YourRepo.update(bumping_backwards_query, dec: [number: -1])
changeset
end)
else
changeset
end
end
I haven’t run this code, but it should be essentially what you need.
FWIW there are strategies around sorted relationships that use sparse sets of numbers and/or floating point numbers very cleverly to avoid having to rebalance things often. I’d like to make an extension at some point that will add that capability automatically to a relationship.
Also Liked
zac
One more thing; there is no dec: but there is an inc: where the argument is negative. Otherwise, looks good! Thanks, appreciate the pointer to dropping into Ecto. Hadn’t done that before / didn’t realize it was so easy to do.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








