Создание карты 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

Как видите, модель категорий может ссылаться на себя через атом подкатегорий.

Вот вид, связанный с этой моделью:

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, когда они не были предварительно загружены.

Наконец, вот две мои функции контроллера, которые вызывают это представление:

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 ограничена двумя уровнями вложенности из-за того, как я использую предварительную загрузку:

{
  "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.

Я знаю, что мне нужна рекурсивная магия, но, поскольку я новичок как в функциональном программировании, так и в Эликсире, я не могу осмыслить это. Любая помощь приветствуется.


person user1112789    schedule 03.09.2015    source источник


Ответы (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.

person José Valim    schedule 03.09.2015
comment
Прекрасно работает. :) Стоит отметить, что репозиторий проекта должен быть добавлен как псевдоним, так как по умолчанию в представлении их нет. Я постараюсь позаботиться об этом на стороне БД, а не в приложении. Спасибо! - person user1112789; 04.09.2015
comment
@ jose-valim - это более красивая альтернатива этой проблеме? - person user2290820; 26.01.2017