endymion

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.

Showing Posts 1 to 8

LostKobrakai

LostKobrakai

The first sentence of the documentation states:

Provides a function executed by the repository on insert/update/delete.

For Ecto.Changeset.apply_changes there’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

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_changes do last minute modifications the changeset, and in that regard, I also found it a bit surprising that apply_changes doesn’t apply prepare_changes.

I also use Ecto.Changeset a lot to validate and prepare data for other databases (like Cassandra), so an apply_changes_with_prepare would be useful for me.

endymion

endymion OP

In addition to @cjbottaro’s comment, using changesets to validate Phoenix forms makes the use case for apply_changes_with_prepare even more obvious.

A form would have two event triggers validate and submit. You would only want to directly apply the modification in prepare_changes during the submit event and not the validate event. This has first class support if you insert your changeset into a DB but not if you simple need to run apply_changes/1 and 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_changes

LostKobrakai

LostKobrakai

You can always call the callback by yourself (acting as your own repository). But this is not a bug. Ecto.Changeset.apply_changes can easily be called in a context, which is not meant to execute those callbacks.

changeset = Enum.reduce(changeset.prepare, changeset, fn c, acc -> c.(acc) end)
Ecto.Changeset.apply_changes(changeset)
endymion

endymion OP

Per the docs prepare is not intended for public consumption. This also complicates matters when having nested changesets with embeds_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_changes to run the prepared changes.

For anyone that needs it, this is the solution I made

  @doc"""
  Runs all prepared functions and apply changes to a changeset. See implimentation of run/1 for more details
  """
  def apply_with_prepared_changes(%Ecto.Changeset{} = changeset) do
    run(changeset)
    |> Ecto.Changeset.apply_changes()
  end

  @doc """
  Runs all prepared functions on a changeset and its associated changesets.
  **WARNING** This should not called before saving a record through Ecto. This will result in
  duplicate prepared function calls and have bad consequences.
  """
  defp run(%Ecto.Changeset{} = changeset) do
    if changeset.changes != %{} do
      Enum.reduce(changeset.changes, changeset, fn {field, value}, acc ->
        new_changeset = run_prepared_fun(value)
        put_embed_or_change(acc, field, new_changeset)
      end)
    else
      changeset
    end
  end
  defp run(changeset), do: changeset

  defp run_prepared_fun(changesets) when is_list(changesets) do
    Enum.map(changesets, fn embeded_changeset ->
      run_prepared_fun(embeded_changeset)
    end)
  end
  defp run_prepared_fun(%Ecto.Changeset{} = changeset) do
    Enum.reduce(changeset.prepare, changeset, fn fun, acc ->
      fun.(acc)
    end)
    |> run()
  end
  defp run_prepared_fun(changeset), do: changeset

  defp put_embed_or_change(changeset, field, value) do
    schema = changeset.data.__struct__
    field_type = schema.__schema__(:type, field)

    case field_type do
      {_, {Ecto.Embedded, _}} -> Ecto.Changeset.put_embed(changeset, field, value)
      {_, {Ecto.Association, _}} -> Ecto.Changeset.put_assoc(changeset, field, value)
      _ -> Ecto.Changeset.put_change(changeset, field, value)
    end
  end
LostKobrakai

LostKobrakai

Ecto has quite a strict division between pure Ecto.Changeset apis and sideeffects, which run in Repo apis. prepare_changes is 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 doing Ecto.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

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_embed and cast_assoc?

joram

joram

There is not. Ecto internally uses Ecto.Embedded.prepare with embeds from schema.__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.

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
mnkhod
So i have been using ash framework for a while and i love it. However currently the issue im having with ash framework is the error handl...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews