I've a simple before_save transformation and learned that phoenix uses Ecto changest for this task.
My Stage
model has a position
attribute which defaults to the current maximum + 1
so tried to implement this as follows:
Stage Model:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, @required_fields, @optional_fields)
|> validate_required([:name])
|> set_position
end
defp set_position(current_changeset) do
# get current max position from db
max_position = Repo.one(
from s in Stage,
select: fragment("COALESCE(MAX(?),0)", s.position)
)
case current_changeset do
%Ecto.Changeset{valid?: true} ->
put_change(current_changeset, :position, max_position+1)
_ ->
current_changeset
end
end
Which works fine when inserting one by one records but fails in bulk insertion; for example in below seed
file.
Seed
alias MyApp.{Repo, Post}
[
%{name: "Requirements"},
%{name: "Quotation"},
%{name: "Development"},
%{name: "Closing"}
]
|> Enum.map(&Post.changeset(%Post{}, &1))
|> Enum.each(&Repo.insert!(&1))
Expected/Current Behavior:
If current max position is say 7
, for all inserted 4 records above, position will be set to 8
instead of 8,9,10,11
respectively! That's because the first pipe will prepare all changests then insert them!
Is the way i am seeding wrong? or the changeset? How can i re-design this so the behavior is the same regardless of how i do insertions? any feedback to improve how i am doing it is appreciated!
You can use Ecto.Changeset.prepare_changes/2
to run arbitrary computation inside the database transaction of that changeset. Your set_position/1
function has the correct argument/return value (changeset -> changeset), so you just need to change:
|> set_position
to
|> prepare_changes(&set_position/1)
and set_position
will now be executed in the same transaction as and just before your Post is inserted, instead of being executed while the changeset is created.