2015-09-03 13 views
13

ho un modello di Ecto in quanto tale:Costruire una mappa JSON per un modello di Ecto autoreferenziale

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 

Come si può vedere il modello Categoria sé può fare riferimento tramite l'atomo sottocategorie.

Ecco la vista associata a questo modello:

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 

Ho un se condizione in sottocategorie in modo che posso giocare bello con il veleno quando non sono precaricati.

Infine, qui miei 2 funzioni del controller che richiamano questa vista:

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 

La funzione show funziona bene:

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

Tuttavia, la funzione showWithChildren è limitato a 2 livelli di nidificazione perché di come utilizzo il preloading:

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

Ad esempio, anche l'articolo di categoria 11 sopra ha sottocategorie, ma non riesco a raggiungerle. Queste sottocategorie possono anche avere sottocategorie stesse, quindi la profondità potenziale della gerarchia è n.

Sono consapevole del fatto che ho bisogno di qualche magia ricorsiva, ma poiché sono nuovo sia per la programmazione funzionale che per l'elisir, non riesco a capirlo. Qualsiasi aiuto è molto apprezzato.

risposta

9

Si può considerare fare il precarico nella vista, quindi funziona in modo ricorsivo:

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 

Tenete a mente questo non è l'ideale per due motivi:

  1. Idealmente non si vuole per fare interrogazioni nelle viste (ma questo è ricorsivo, quindi è più facile da includere nel rendering della vista)

  2. Stai per emettere più query per il secondo livello di sottocategorie

C'è un libro intitolato SQL Antipatterns e, se non mi sbaglio, copre come scrivere strutture ad albero. Il tuo esempio è esposto come antipattern in uno dei capitoli gratuiti. È un libro eccellente ed esplorano soluzioni per tutti gli antipattern.

PS: si desidera show_with_children e non showWithChildren.

+2

Funziona meravigliosamente. :) Vale la pena ricordare che il repository del progetto deve essere aggiunto come alias, in quanto le view non le hanno per impostazione predefinita. Cercherò di occuparmene sul lato DB piuttosto che sull'applicazione. Grazie! – user1112789

+0

@ jose-valim è l'alternativa più carina a questo problema? – user2290820

Problemi correlati