Keep related writes together with `prepare_changes/2`

almirsarajcic

almirsarajcic

1 hour ago

0 comments

If a database write belongs to a changeset operation, prepare_changes/2 can run it in the same transaction. A common example is updating a counter when a record is inserted.

defmodule MyApp.Blog.Comment do
  use Ecto.Schema

  import Ecto.Changeset
  import Ecto.Query

  alias MyApp.Blog.Post

  schema "comments" do
    field :body, :string
    belongs_to :post, Post

    timestamps()
  end

  @spec changeset(t(), map()) :: Ecto.Changeset.t()
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [:body, :post_id])
    |> validate_required([:body, :post_id])
    |> prepare_changes(fn changeset ->
      post_id = get_field(changeset, :post_id)
      query = from p in Post, where: p.id == ^post_id
      changeset.repo.update_all(query, inc: [comments_count: 1])

      changeset
    end)
  end
end

Ecto runs the callback after validation and only for a valid changeset. Because the callback and the insert share a transaction, a constraint error on the insert also rolls back the counter update.

The callback receives the repo through changeset.repo, so the schema does not need to reference MyApp.Repo directly. It must return a changeset and may make further changes to it before returning.

Without the callback, the two writes happen separately:

# These writes do not succeed or fail as a unit.
{:ok, comment} = Repo.insert(changeset)
Repo.update_all(from(p in Post, where: p.id == ^comment.post_id), inc: [comments_count: 1])

# The prepare callback makes Repo.insert/1 the only call needed here.
Repo.insert(changeset)

Use Ecto.Multi when the caller coordinates a larger workflow. prepare_changes/2 fits an operation that should accompany this changeset wherever it is used, whether that is from a context function, a seed script, or an import.

The callback runs for changesets passed to Repo.insert/2, Repo.update/2, and Repo.delete/2. It does not run for Repo.insert_all/3, which bypasses changesets.

Ecto.Changeset.prepare_changes/2 docs

Comments (0)

Sign in with GitHub to join the discussion