Построение карты JSON для самоссылающейся модели Ecto
У меня есть модель 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
Как вы можете видеть, модель Category может ссылаться на себя через атом подкатегорий.
Вот вид, связанный с этой моделью:
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, я не могу обернуть голову вокруг этого. Любая помощь с благодарностью.
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
Имейте в виду, что это не идеально по двум причинам:
В идеале, вы не хотите делать запросы в представлениях (но это рекурсивно, поэтому в рендеринге представления легче совмещать)
Вы собираетесь отправить несколько запросов для второго уровня подкатегорий
Есть книга под названием " Антипаттерны SQL", и, если я не ошибаюсь, она описывает, как писать древовидные структуры. Ваш пример представлен как антипаттерн в одной из бесплатных глав. Это отличная книга, и они исследуют решения для всех antipatterns.
PS: хочешь show_with_children
и не showWithChildren
,