Search code examples
elixirecto

Ecto - updating nested embed


I can't update nested settings with ecto, I either get "no changes" changeset or errors. Migration:

def change do
  create table(:trees) do
  ...
  add :settings, :map

Settings looks like:

defmodule Final.TreeSettings do
  use Ecto.Schema

  embedded_schema do
    ...
    field :columns, :map       
    timestamps
  end
end

Notice the nested columns map.

I can insert new Tree row easily like:

changeset = Tree.changeset(%Tree{}, %{user_id: user_id, name: x})
      |> Ecto.Changeset.put_embed(:settings, treeSettings)

But updating it the same way doesn't work:

get_tree = Repo.one! from p in Tree, where: p.name == ^tree["name"], where: p.user_id == ^user_id
settingss = get_tree.settings
settingss = Kernel.update_in(settingss.columns[tree["setting"]][tree["type"]], fn x -> "asdasd" end)
# IO.inspect(settingss) shows correct changes here.
changeset =
    get_tree
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_embed(:settings, settingss)    
    IO.inspect changeset

Gives:

#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Final.Tree<>,
 valid?: true>

Solution

  • I think you can wrap embed into Ecto.Changeset.change/2 before use it in Ecto.Changeset.html#put_embed/4

    Full example:

    Migration:

    defmodule Final.Repo.Migrations.CreateTree do
      use Ecto.Migration
    
      def change do
        create table(:trees) do
          add :settings, :map
        end
      end
    end
    

    Model Tree:

    defmodule Final.Tree do
      use Final.Web, :model
    
      schema "trees" do
        embeds_one :settings, Final.TreeSettings
      end
    
      def changeset(struct, params \\ %{}) do
        struct
        |> cast(params, [])
        |> cast_embed(:settings)
      end
    end
    

    Model TreeSettings:

    defmodule Final.TreeSettings do
      use Final.Web, :model
    
      embedded_schema do
        field :columns, :map
      end
    
      def changeset(struct, params \\ %{}) do
        struct
        |> cast(params, [:columns])
      end
    end
    

    The test:

    defmodule Final.TreeTest do
      use Final.ModelCase
    
      alias Final.Tree
    
      test "updating nested embed" do
        Repo.insert! Tree.changeset(%Tree{}, %{settings: %{columns: %{"key" => "value", "key2" => "value2"}}})
    
        tree = Repo.one(Tree)
        settings_changeset = tree.settings
        |> Ecto.Changeset.change(%{columns: %{tree.settings.columns | "key" => "new value"}})
    
        changeset = tree
        |> Ecto.Changeset.change
        |> Ecto.Changeset.put_embed(:settings, settings_changeset)
        Repo.update! changeset
    
        assert Repo.one(Tree).settings.columns == %{"key" => "new value", "key2" => "value2"}
      end
    end