【问题标题】:Building a JSON map for a self-referencing Ecto model为自引用的 Ecto 模型构建 JSON 映射
【发布时间】:2015-09-03 15:53:53
【问题描述】:

我有一个这样的 Ecto 模型:

defmodule Project.Category do
  use Project.Web, :model

  schema "categories" do
    field :name, :string
    field :list_order, :integer
    field :parent_id, :integer
    belongs_to :menu, Project.Menu
    has_many :subcategories, Project.Category, foreign_key: :parent_id
    timestamps
  end

  @required_fields ~w(name list_order)
  @optional_fields ~w(menu_id parent_id)

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

如您所见,类别模型可以通过子类别原子引用自身。

这是与此模型关联的视图:

defmodule Project.CategoryView do
  use Project.Web, :view

  def render("show.json", %{category: category}) do
    json = %{
      id: category.id,
      name: category.name,
      list_order: category.list_order
      parent_id: category.parent_id
    }
    if is_list(category.subcategories) do
      children = render_many(category.subcategories, Project.CategoryView, "show.json")
      Map.put(json, :subcategories, children)
    else
      json
    end
  end
end

我对子类别有一个 if 条件,以便在未预加载 Poison 时我可以玩得很好。

最后,这是我调用此视图的 2 个控制器函数:

defmodule Project.CategoryController do
  use Project.Web, :controller

  alias Project.Category

  def show(conn, %{"id" => id}) do
    category = Repo.get!(Category, id)
    render conn, "show.json", category: category
  end

  def showWithChildren(conn, %{"id" => id}) do
    category = Repo.get!(Category, id)
               |> Repo.preload [:subcategories, subcategories: :subcategories]
    render conn, "show.json", category: category
  end
end

show 函数工作正常:

{
  "parent_id": null,
  "name": "a",
  "list_order": 4,
  "id": 7
}

但是,由于我使用预加载的方式,我的 showWithChildren 函数仅限于 2 级嵌套:

{
  "subcategories": [
    {
      "subcategories": [
        {
          "parent_id": 10,
          "name": "d",
          "list_order": 4,
          "id": 11
        }
      ],
      "parent_id": 7,
      "name": "c",
      "list_order": 4,
      "id": 10
    },
    {
      "subcategories": [],
      "parent_id": 7,
      "name": "b",
      "list_order": 9,
      "id": 13
    }
  ],
  "parent_id": null,
  "name": "a",
  "list_order": 4,
  "id": 7
}

例如,上面的类别项目 11 也有子类别,但我无法访问它们。这些子类别本身也可以有子类别,因此层次结构的潜在深度为 n

我知道我需要一些递归魔法,但由于我对函数式编程和 Elixir 都是新手,所以我无法完全理解它。非常感谢任何帮助。

【问题讨论】:

    标签: json elixir phoenix-framework ecto


    【解决方案1】:

    您可以考虑在视图中进行预加载,因此它以递归方式工作:

    def render("show.json", %{category: category}) do
      %{id: category.id,
        name: category.name,
        list_order: category.list_order
        parent_id: category.parent_id}
      |> add_subcategories(category)
    end
    
    defp add_subcategories(json, %{subcategories: subcategories}) when is_list(subcategories) do
      children =
        subcategories
        |> Repo.preload(:subcategories)
        |> render_many(Project.CategoryView, "show.json")
      Map.put(json, :subcategories, children)
    end
    
    defp add_subcategories(json, _category) do
      json
    end
    

    请记住,这并不理想,原因有两个:

    1. 理想情况下,您不想在视图中进行查询(但这是递归的,因此更容易在视图渲染中捎带)

    2. 您将为第二级子类别发出多个查询

    有一本书叫SQL Antipatterns,如果我没记错的话,它介绍了如何编写树结构。您的示例在其中一个免费章节中作为反模式公开。这是一本很棒的书,他们探索了所有反模式的解决方案。

    PS:你想要show_with_children 而不是showWithChildren

    【讨论】:

    • 效果很好。 :) 值得一提的是,项目的 repo 应该作为别名添加,因为默认情况下视图没有它们。我将考虑在数据库端而不是应用程序上解决这个问题。谢谢!
    • @jose-valim ltree 是不是这个问题的更漂亮的替代方案?
    猜你喜欢
    • 2015-09-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-09
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多