Reputation: 11
So I've just started fiddeling around with Phoenix, and Elixir. So I have reached the point were I am trying to get a working rest-api endpoint to work with a prerequisite JSON.
So I have this module:
defmodule MyApp.Housing.Part do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, []}
schema "parts" do
field :level, :integer
field :title, :string
belongs_to :parent, MyApp.Housing.Part
has_many :children, MyApp.Housing.Part, foreign_key: :parent_id
timestamps()
end
def changeset(part, params \\ %{}) do
part
|> cast(params, [:title, :level, :id, :parent_id])
|> put_assoc(:children, required: false)
|> put_assoc(:parent, required: false)
|> validate_required([:title, :level, :id])
end
end
And the module in which the table is created
defmodule MyApp.Repo.Migrations.CreateParts do
use Ecto.Migration
def change do
create table(:parts, primary_key: false) do
add :id, :integer, primary_key: true
add :title, :string
add :level, :integer
add :parent_id, references(:parts)
add :children, references(:parts)
timestamps()
end
create index(:parts, [:children])
create index(:parts, [:parent_id])
end
end
The inteded functionallity is for a part to be able to have multiple children but only one parent. And these are defined in a JSON like this:
{"id": 10,
"title": "Matt",
"level": 0,
"children": [],
"parent_id": null}
So my problem is the following:
{"part":{}}
otherwise the ActionClauseError gets thrown.children":["is invalid"]
. And I can't figure out how to get a valid one, if I did I probably could figure out the problem. I might take the wrong approach here but would gladly accept any help.
Upvotes: 1
Views: 1847
Reputation: 433
As mentioned by @steve-pallen, it's not necessary to store any references to children
in the database. Determining whether or not a Part
is a parent or child, as well as which Part
s are its children or which Part
is its parent can be determined fully by the parent_id
field.
You described in your question that each Part
"can only have one parent, but multiple children". It's not explicit in your question how many levels the relationship allows: i.e. can a Part
be both a parent and a child? In which case, there would be potentially infinite levels of nesting:
part1
|- part2
|- part3
|- part4
In this case, part1
is the parent of part2
, part2
is itself the parent of part3
, etc. I'm going to assume for my answer that there is no limit to the amount of nesting.
Given this case, your schema definition is 100% correct:
belongs_to :parent, MyApp.Housing.Part
has_many :children, MyApp.Housing.Part, foreign_key: :parent_id
I think the primary issue is with your changeset function. Remember that with put_assoc/3
, it's expected that all the models referenced by parent
and children
already exist in the DB (see the docs for cast_assoc/3). For simplicity I suggest that you don't use put_assoc
or cast_assoc
and instead manage each model in isolation. If you change your changeset function to this (I've removed id
since it's not necessary):
def changeset(part, params \\ %{}) do
part
|> cast(params, [:title, :level, :parent_id])
|> validate_required([:title, :level])
end
Then you can build the nested relationship I showed above by doing 4 inserts in isolation (much easier to reason about, and probably more in line with how you'd handle DB updates from a form or script):
part1 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part1", level: 0, parent_id: nil})
|> Repo.insert!()
part2 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part2", level: 0, parent_id: part1.id})
|> Repo.insert!()
part3 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part3", level: 0, parent_id: part2.id})
|> Repo.insert!()
part4 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part4", level: 0, parent_id: part3.id})
|> Repo.insert!()
Assuming we want to get part2
, we can load it along with its parent and children like this:
part2 = Repo.preload(part2, [:parent, :children])
# part2.parent == %MyApp.Housing.Part{title: "part1", ...}
# part2.children == [%MyApp.Housing.Part{title: "part3", ...}]
Hope this helps!
Upvotes: 1
Reputation: 4507
The first thing you need to fix is the migration. You don't want the children field since that is a has_many relationship and is handled by the parent_id field in the children. It should look like this:
defmodule MyApp.Repo.Migrations.CreateParts do
use Ecto.Migration
def change do
create table(:parts, primary_key: false) do
add :id, :integer, primary_key: true
add :title, :string
add :level, :integer
add :parent_id, references(:parts)
timestamps()
end
create index(:parts, [:parent_id])
end
end
Handling the children in the changeset depends on a couple things.
Upvotes: 0