使用Ecto在Phoenix中进行自引用关联

时间:2018-10-10 18:21:27

标签: postgresql elixir phoenix-framework ecto

所以我刚开始和Phoenix和Elixir呆滞。因此,我已经达到了要尝试使用可运行的rest-api端点与必备JSON一起工作的地步。

所以我有这个模块:

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

以及创建表的模块

 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

该功能性是为了使部件可以有多个孩子,但只有一个父母。这些是在这样的JSON中定义的:

{"id": 10,
    "title": "Matt",
    "level": 0,
    "children": [],
    "parent_id": null}

所以我的问题如下:

  • “变更集”要求传入的对象看起来像{"part":{}},否则引发ActionClauseError。
  • 当按上述方式定义对象时,出现错误children":["is invalid"]。而且我不知道如何获得一个有效的密码,如果我这样做了,我可能可以找出问题所在。

我在这里可能采取了错误的方法,但是很乐意接受任何帮助。

2 个答案:

答案 0 :(得分:0)

您需要解决的第一件事是迁移。您不希望children字段,因为那是一个has_many关系,并由child中的parent_id字段处理。它应该看起来像这样:

 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

处理变更集中的子项取决于几件事。

  • 有孩子时传入的有效载荷是什么样的?
  • 孩子列表中会有新孩子还是现有孩子?

答案 1 :(得分:0)

如@ steve-pallen所述,没有必要在数据库中存储对children的任何引用。 Part可以完全确定Part是父母还是孩子,以及哪个Part是它的孩子或哪个parent_id是它的父母。字段。

您在问题中描述,每个Part“只能有一个父母,但可以有多个孩子”。在您的问题中,关系允许达到多少级别尚不明确:即Part既可以是父母,也可以是孩子?在这种情况下,嵌套的级别可能是无限的:

part1
  |- part2
    |- part3
      |- part4

在这种情况下,part1part2的父级,part2本身是part3的父级,依此类推。我假设我的回答是嵌套数量没有限制。

在这种情况下,您的架构定义是100%正确的:

belongs_to :parent, MyApp.Housing.Part
has_many :children, MyApp.Housing.Part, foreign_key: :parent_id

我认为主要问题在于变更集功能。请记住,对于put_assoc/3,期望parentchildren引用的所有模型已经存在于数据库中(请参阅cast_assoc/3的文档)。为简单起见,我建议您不要使用put_assoc cast_assoc,而应单独管理每个模型。如果您将changeset函数更改为此(我已删除id,因为它是不必要的):

def changeset(part, params \\ %{}) do
  part
  |> cast(params, [:title, :level, :parent_id])
  |> validate_required([:title, :level])
end

然后,您可以通过单独进行4次插入来构建我上面显示的嵌套关系(更容易推断,并且更可能与您从表单或脚本处理数据库更新的方式保持一致):

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!()

假设我们要获取part2,则可以将其及其父级和子级一起加载:

part2 = Repo.preload(part2, [:parent, :children])
# part2.parent == %MyApp.Housing.Part{title: "part1", ...}
# part2.children == [%MyApp.Housing.Part{title: "part3", ...}]

希望这会有所帮助!