Using Repo.insert_all when your schema has embeds_many

Not so much a question as a discovery. This is true as of Ecto 2.2. If you need to perform an insert_all and the target schema has embeds_many to an embedded_schema…here’s how to make it work.

defmodule ImageTest do
  defmodule Image do
    use Ecto.Schema
    import Ecto

    embedded_schema do
      field :url, :string
      field :purpose, :string
      field :color, :string
    end

    def changeset(image, attrs), do: cast(image, attrs, [:url, :color, :purpose])
  end

  use Ecto.Schema
  import Ecto

  defmodule Migration do
    use Ecto.Migration

    def change do
      create table(:image_test, primary_key: false) do
        add :id, :binary_id, primary_key: true
        add :images, :jsonb, null: false, default: fragment("'{}'::jsonb")
      end
    end
  end

  schema "image_test" do
    embeds_many :images, Image

    timestamps()
  end

  @required_fields ~w(images)a

  def changeset(struct, params), do: cast(struct, params, @required_fields)
end

# Assumes a `Repo`.
images1 = [%{url: "giphy.com"}, %{color: "blue"}]
images2 = [%{url: "imgur.com"}, %{color: "red"}]

# This won’t work
# Repo.insert_all(ImageTest, %{images: images1, images2})

images1 = Enum.map(images1, &struct(ImageTest.Image, &1))
images2 = Enum.map(images2, &struct(ImageTest.Image, &1))
# This works.
Repo.insert_all(ImageTest, %{images: images1, images2})

It feels weird that this is what we have to do for this to work, but I’m happy to have discovered it.

2 Likes

THANK YOU!!!

I have been struggling with insert_all with an embed and could not resolve the error:

** (Ecto.ChangeError) value {:ok, %{...}} for ... in insert_all does not match type {:embed, %Ecto.Embedded{cardinality: :one, field: :preferences, on_cast: nil, on_replace: :update, ordered: true, owner: …, related: …, unique: true}}

Casting the embed changeset changes back to a struct within the insert_all operation solved it.