Nested changeset update in Ecto

Hello, I have a User struct and to levels down inside of it, I have an Product struct.

The make up is like this `User -> Accounts -> Account -> Products -> Product

How do I modify this

user
        |> Ecto.Changeset.change(%{inserted_at: new_time}
)

to be able to change the inserted_at of the product item? Note, account is associated with user and product is associated with account.

I’m not sure that it is helpful to think of the Product as “inside” the user. It’s a first class entity that can be updated on its own.

product = get_product_in_user(user, ... etc)
product |> change(%{}) |> Repo.update!

Correct; however; I am using this inside of a test.

I have example_function(user) and product is modified inside that function. And so I am testing different edge cases.

Every changeset is nested. So there is the changeset of the Product; you should access that nested changeset and call put_change I guess? (or directly update the :changes map)

Every changeset is nested into :changes of the parent changeset.

I don’t have strong skills with Ecto, just an idea:)

I’m curious. You mention " to be able to change the inserted_at of the product item?". If there are many accounts for users and many products for accounts, how to know which product is “the” product?

To answer the question you I think you could do something like this (I haven’t actually verified that works sorry):

changes = %{
  accounts: [
    %{
      products: [
        %{inserted_at: "YOUR DATETIME HERE"}
      ]
    }
  ]
}
Ecto.Changeset.change(%User{}, changes)
|> Ecto.Changeset.cast_assoc(:accounts, with: fn account, changes ->
  Ecto.Changeset.change(account, changes)
  |> Ecto.Changeset.cast_assoc(:products, with: fn product, changes ->
    Ecto.Changeset.change(product, changes)
  end)
end)

However I wrote a library that makes this kind of thing simple though:

changes = %{
  accounts: [
    %{
      products: [
        %{inserted_at: "YOUR DATETIME HERE"}
      ]
    }
  ]
}
EctoMorph.generate_changeset(%User{}, changes)

You’d have to be sure you were updating the correct product and account though, as in that the changes

It says that the struct is missing is missing primary key value. Note: I pass in the user rather than generic %User{}

Hmm that seems like a different error I tried this and it worked:


defmodule Product do
  use Ecto.Schema

  schema "products" do
    field(:inserted_at, :date)
  end
end

defmodule Account do
  use Ecto.Schema

  schema "accounts" do
    has_many(:products, Product)
  end
end

defmodule User do
  use Ecto.Schema

  schema "user" do
    has_many(:accounts, Account)
  end
end

changes = %{
  accounts: [
    %{
      products: [
        %{inserted_at: ~D[2020-03-03]}
      ]
    }
  ]
}

Ecto.Changeset.change(%User{id: "123"}, changes)
|> Ecto.Changeset.cast_assoc(:accounts,
  with: fn account, changes ->
    Ecto.Changeset.change(account, changes)
    |> Ecto.Changeset.cast_assoc(:products,
      with: fn product, changes ->
        Ecto.Changeset.change(product, changes)
      end
    )
  end
)