2015-09-03 3 views
13

У меня есть модель экта как таковые:Построение карты в формате JSON для автореферентных экто моделей

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 

У меня есть если условия на подкатегории, так что я могу играть хорошо с ядом, когда они не поджаты.

Наконец, здесь мои 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, я не могу обвести вокруг себя голову. Любая помощь приветствуется.

ответ

9

Вы можете рассмотреть делать предзагрузку в представлении, поэтому она работает рекурсивно:

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.

+2

Работает чудесно. :) Стоит упомянуть, что репозит проекта должен быть добавлен как псевдоним, так как у представления нет их по умолчанию. Я буду следить за этим на стороне БД, а не в приложении. Благодаря! – user1112789

+0

@ jose-valim - это более красивая альтернатива этой проблеме? – user2290820