Proposal: skip the implicit transaction on Repo.insert/update when embeds don't need one

27 views
Skip to first unread message

Travis Vander Hoop

unread,
Sep 25, 2026, 7:48:30 PM (13 days ago) Sep 25
to elixir-ecto
Hi all,

I'd like to propose a way to avoid the implicit transaction that Repo.insert and Repo.update open when a changeset changes an embed.

Today, `wrap_in_transaction/8` in `Ecto.Repo.Schema` opens a transaction whenever a changeset either changes an association/embed or has `prepare_changes` callbacks. For our embeds (which are `jsonb` columns under the hood), that turns one insert statement into three round trips:

    # migration
    create table(:posts) do
      add :title, :text
      add :meta, :jsonb
    end

    defmodule Meta do
      use Ecto.Schema

      embedded_schema do
        field :source, :string
      end
    end

    defmodule Post do
      use Ecto.Schema

      schema "posts" do
        field :title, :string
        embeds_one :meta, Meta
      end
    end

    Repo.insert!(%Post{title: "hello", meta: %Meta{source: "web"}})
    # begin
    # INSERT INTO "posts" ("meta","title") VALUES ($1,$2) RETURNING "id"
    # commit

    Repo.insert!(%Post{title: "hello"})
    # INSERT INTO "posts" ("title") VALUES ($1) RETURNING "id"

(Ecto 3.14.2, ecto_sql 3.14.0, postgrex 0.22.4, PostgreSQL 18.4.)

Again, our `embeds_one` is a jsonb column, so the embed is written by the same INSERT as the rest of the row. Nothing else needs to be atomic with it, and the single statement is already atomic on its own, so the BEGIN and COMMIT add round trips without adding value.

As far as I can tell, the transaction exists for embedded `prepare_changes` callbacks, which `Ecto.Embedded.prepare/4` runs with the repo, and which the `prepare_changes` docs guarantee run inside the same transaction as the operation. I see two ways to keep that guarantee while dropping the transaction when it has nothing to protect.

Option 1: only treat an embed as needing a transaction when it has callbacks

By the time `wrap_in_transaction/8` runs, changed embeds are already changesets, so Ecto could open the transaction only when an embed changeset (top level or nested) has `prepare_changes` callbacks. Changed associations and the parent changeset's own `prepare_changes` callbacks would still open one, as they do today. The benefit of this approach is that everyone would get this new (and, I think, non-breaking) behavior with no code changes.

Option 2: let the caller opt out

    Repo.insert(changeset, implicit_transaction: false)

With the option set, Ecto would run the insert/update without wrapping it. `prepare_changes` callbacks would still run, just outside a transaction, which the caller has opted into. The option name is only a suggestion, but I think it communicates the intent.

I'm happy to hear if I've missed a reason the transaction is needed here, and happy to send a PR if folks are onboard with either proposed option.

Thanks,
Travis Vander Hoop

A.I. Disclosure: I used Opus 5.5 to research and help draft this email.

José Valim

unread,
Sep 26, 2026, 4:15:20 AM (13 days ago) Sep 26
to elixi...@googlegroups.com

Yes, this is a bug and must be fixed!


--
You received this message because you are subscribed to the Google Groups "elixir-ecto" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-ecto...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/elixir-ecto/9aff4495-3af3-40a7-aa88-6286977d5fa2n%40googlegroups.com.

Travis Vander Hoop

unread,
Sep 27, 2026, 11:35:08 AM (12 days ago) Sep 27
to elixir-ecto
Opened a small PR to address the issue. Happy to make any changes requested/required: https://github.com/elixir-ecto/ecto/pull/4797
Reply all
Reply to author
Forward
0 new messages