endymion
I am wondering why when I ran Ecto.Changeset.apply_changes(changeset) the prepared changes function on that changeset did not run. I have a changeset that I am building from a Phoenix form; after validation occurs and user submits the form, I want to apply those params to my struct with a prepared change to increment a value of one of the inputs.
Simple example demonstrating the unexpected behavior of apply_changes/1
We have a sample struct. It casts some params then adds a prepare_changes function to that changeset. When calling apply_changes that prepare_changes function is not run so our resulting struct is incorrect.
Example:
defmodule InputData do
use Ecto.Schema
@primary_key false
embedded_schema do
field :value, :integer
end
end
%InputData{value: nil}
|> Ecto.Changeset.cast(%{"value" => "1"}, [:value])
|> Ecto.Changeset.validate_number(:value, [greater_than: 0])
|> Ecto.Changeset.prepare_changes(fn changeset ->
incremented_value = Ecto.Changeset.get_field(:value, 0) + 1
Ecto.Changeset.put_change(changeset, :value, incremented_value)
end)
|> Ecto.Changeset.apply_changes()
The output of this is not the expected %InputData{value: 2} where the params casts the value to 1 then is incremented again by 1 in the prepare_changes/2. Why is this? Since apply_changes/1 results in a struct and not a changeset there is no risk of firing the prepared_changes functions again if the developer needed to to a Repo insert. In my case I am never inserting this struct into a database and thus need these prepared_changes to run when calling apply_changes.
In summary this feels like a bug or missing implementation in Ecto Changesets that I seem unable to come up with a convincing argument as to why it shouldn’t behave like this.
Trending in Questions
Other Trending 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
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixir-ls
- #ai
- #elixirconf-us
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 8- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
LostKobrakai
The first sentence of the documentation states:
For
Ecto.Changeset.apply_changesthere’s no repository involved and it’s neither of those mentioned actions either.Usually prepare_changes is used to execute code within the same transaction as what the changeset applies to the database, which needs to be provided as a callback as that transaction is only started later (by the repo). If you don’t have a transaction you can directly apply the changes to the changeset in place. No need to delay that.
cjbottaro
The docs show an interesting example, where the changeset isn’t modified at all and there are just side effects (updating the database). Most of my uses of
prepare_changesdo last minute modifications the changeset, and in that regard, I also found it a bit surprising thatapply_changesdoesn’t applyprepare_changes.I also use Ecto.Changeset a lot to validate and prepare data for other databases (like Cassandra), so an
apply_changes_with_preparewould be useful for me.endymion
In addition to @cjbottaro’s comment, using changesets to validate Phoenix forms makes the use case for
apply_changes_with_prepareeven more obvious.A form would have two event triggers
validateandsubmit. You would only want to directly apply the modification inprepare_changesduring thesubmitevent and not thevalidateevent. This has first class support if you insert your changeset into a DB but not if you simple need to runapply_changes/1and store else where such as Cassandra.I do not want to do my increment to the value after each validation step, only at the end step during
apply_changesLostKobrakai
You can always call the callback by yourself (acting as your own repository). But this is not a bug.
Ecto.Changeset.apply_changescan easily be called in a context, which is not meant to execute those callbacks.endymion
Per the docs
prepareis not intended for public consumption. This also complicates matters when having nested changesets withembeds_many. Our use case has 5 levels of nesting which means I needed to create a recursive solution.When doing this though I couldn’t help but feel that this should just be first class supported as this is already implemented when saving the changeset through Ecto.
Also just because the current functionality doesn’t exist doesn’t mean it shouldn’t and there isn’t a valid use case. There should be a separate function or an option on
apply_changesto run the prepared changes.For anyone that needs it, this is the solution I made
LostKobrakai
Ecto has quite a strict division between pure
Ecto.Changesetapis and sideeffects, which run inRepoapis.prepare_changesis meant to allow for sideeffects to happen - hence the coupling to a repository, which provides the context for the sideeffects to succeed and happen in the same transaction. Take the example placed in the docs. You don’t want to run this when doingEcto.Changeset.apply_changes. It would change your db in the process on an api, which is not meant to access the database.I can see how it would be useful, but I’d argue that you’re using prepare_changes for something it’s not meant for.
cjbottaro
Do you know if there is an easy way to traverse nested changesets? I.e. a way to use the technique you describe above, but traverse changesets that have several levels of
cast_embedandcast_assoc?joram
There is not. Ecto internally uses
Ecto.Embedded.preparewith embeds fromschema.__schema__(:embeds)but that is private and requires a repo adapter. I’ll note that Ecto actually reduces over the reversed list of prepares, but with one function the order doesn’t matter.The internals where Ecto calls these functions are private and very much in the context of a repo. I’ll concur with @LostKobrakai that this is using prepare_changes for something it isn’t meant for. You should just be making changes to your changeset directly before calling
apply_changes.