Hi,
I’m having some trouble to persist two models related to each other that I receive from a form as nested parameters (Phoenix). The thing is that the nested model also has a relationship to another model, that needs to be supplied after the form is submitted. I read documentation and blog posts but I think something is amiss.
Child belongs to Parent and belongs to AnotherModel. Here are the schemas:
defmodule Models.Parent do
use Ecto.Schema
import Ecto.Changeset
schema "parents" do
field(:description, :string)
has_one(:child, Models.Child)
timestamps()
end
@required_fields [:description]
def changeset(parent, params \\ :empty) do
parent
|> cast(params, @required_fields)
|> validate_required(@required_fields)
|> validate_length(:description, min: 5)
end
end
defmodule Models.Child do
use Ecto.Schema
import Ecto.Changeset
schema "child" do
field(:name, :string)
field(:state, :string)
belongs_to(:parent, Models.Parent)
belongs_to(:another_model, Models.AnotherModel)
timestamps()
end
@required_fields [:name, :state]
def changeset(child, params \\ :empty) do
child
|> cast(params, @required_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:state, ~w[new old])
end
end
defmodule Models.AnotherModel do
use Ecto.Schema
import Ecto.Changeset
schema "another_model" do
field(:name, :string)
has_many(:child, Models.Child)
timestamps()
end
@required_fields [:name]
def changeset(another, params \\ :empty) do
another
|> cast(params, @required_fields)
|> validate_required(@required_fields)
end
end
Upon form submission, these are the parameters received:
%{
"description" => "mmmmm",
"child" => %{
"name" => "some new name"
}
}
Trying to create a Parent changeset and inserting it doesn’t work. There are no errors in Parent, but there is an error in Child - no state set and no AnotherModel set.
Question 1: Is it reasonable to set the state and AnotherModel manually on the parameters? Since I’m not creating separate changesets for Parent and Child, I don’t see another solution. Is there a better way?
Let’s assume I manually added a state and AnotherModel, so now the list of parameters is:
%{
"description" => "mmmmm",
"child" => %{
"name" => "some new name",
"state" => "manually added",
"another_model" => %Models.AnotherModel{...}
}
}
Now creating a Parent changeset and inserting it works. However, Child doesn’t have AnotherModel ID set. That’s because Child cast only name and state.
I can change that so to cast another_model_id as well. We get:
defmodule Models.Child do
...
@required_fields [:name, :state]
@assoc_fields [:another_model_id]
def changeset(child, params \\ :empty) do
child
|> cast(params, @required_fields ++ @assoc_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:state, ~w[new old])
end
end
Question 2: is there a better way? This seems to me rather ugly, and now Child has one preferred relationship with Parent (that doesn’t need cast) and one non-preferred with AnotherModel (that needs cast). I could add parent_id to @assoc_fields (@assoc_fields [:parent_id, another_model_id]), but I suspect this is leading to the wrong direction.
Question 3: since I can’t manipulate the changesets of nested models (I can only add/remove values to the params), are nested parameters the way to go? Or should I rather receive Parent and Child separately, insert Parent, set its ID to Child and set an AnotherModel ID to Child, and then insert Child? This would require Ecto.Multi and some more logic for error handling.
Sorry for the long post, thank you.