Elixir Phoenix - 预装与自我的联系

时间:2017-08-04 03:06:57

标签: elixir phoenix-framework

我在评论模型中有以下内容(Phoenix 1.2):

schema "comments" do
   belongs_to :parent, Test.Comment
   belongs_to :user, Test.User
   belongs_to :post, Test.Post
   has_many :children, Test.Comment, foreign_key: :parent_id
end

在迁移过程中,我添加了:

create table(:comments) do
   add :parent_id, :integer
   add :user_id, references(:users, on_delete: :delete_all)
   add :post_id, references(:posts, on_delete: :delete_all)
end

我想在帖子显示页面上显示博客文章,评论和评论的回复(嵌套评论,如reddit)。

评论和嵌套评论已正确创建,但我无法解析在帖子/展示页面上预先加载的“用户”显示嵌套评论。

所以在post_controller中,show函数我有这个:

post = Post
|> Repo.get(id)
|> Repo.preload(comments: from(c in Comment, order_by: [desc: c.inserted_at]), 
   comments: :user, comments: :parent, comments: :children)

在_comment.html.eex中,我放置的以下行会引发错误#Ecto.Association.NotLoaded<association :user is not loaded>

User: <%= @comment.user.username %>

任何有关此的帮助将不胜感激。

更新1:

输出post = Post |> Repo.get(48) |> Repo.preload(comments: from(c in Comment, order_by: [desc: c.votes_up]), comments: :user, comments: :parent, comments: :children)作为带有评论和回复评论的示例帖子。

%Test.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
category: "new",
comments: [%Test.Comment{__meta__: #Ecto.Schema.Metadata<:loaded, 
"comments">,
body: "hello there", children: [], id: 69,
inserted_at: ~N[2017-07-28 21:52:49.636919],
parent: nil, parent_id: nil,
post: #Ecto.Association.NotLoaded<association :post is not loaded>,
post_id: 48,
updated_at: ~N[2017-07-28 21:52:49.636933],
user: %Test.User{username: "dude",
comments: #Ecto.Association.NotLoaded<association :comments is not 
loaded>,
...},
user_id: 11},
%Test.Comment{__meta__: #Ecto.Schema.Metadata<:loaded, "comments">,
   body: "working there?",
   children: [%Test.Comment{__meta__: #Ecto.Schema.Metadata<:loaded, 
   "comments">,
   body: "real child reply should be seen in the show post page",
   children: #Ecto.Association.NotLoaded<association :children is not 
   loaded>,
   id: 85, inserted_at: ~N[2017-08-03 21:52:37.116894],
   parent: #Ecto.Association.NotLoaded<association :parent is not 
   loaded>,
   parent_id: 70,
   post: #Ecto.Association.NotLoaded<association :post is not loaded>,
   post_id: 48,
   user: #Ecto.Association.NotLoaded<association :user is not loaded>,
   user_id: 5}], 
   user: %Test.User{username: "dude", ...

更新2:

在帖子/节目模板中,我必须显示评论:

<%= for comment <- @post.comments do %>
  <%= render "_comment.html", comment: comment, conn: @conn, post: @post %>
<% end %>

然后在_comment.html部分中我执行以下操作以显示父注释及其嵌套子注释:

<p>User: <%= @comment.user.username %></p>
<p>@comment.body</p>

<% unless Enum.empty?(@comment.children) do %>
  <%= for child_comment <- @comment.children do %>
    <ul class="nested_comment">
      <%= render "_comment.html", comment: child_comment, conn: @conn, post: @post %>
    </ul>
  <% end %>
<% end %>

2 个答案:

答案 0 :(得分:1)

来自docs

  

Repo.preload预加载给定结构或结构上的所有关联。

     

这类似于Ecto.Query.preload / 3,但它允许您在从数据库中提取结构后预加载结构。

     

如果已加载关联,则预加载不会尝试重新加载。

您更好的选择是使用Query.preload,因为您无论如何都在同一个管道中。

post = Repo.first(from p in Post,
         join: c in assoc(p, :comments),
         join: u in assoc(c, :user),
         where p.id = ^id,
         preload: [comments: {c, user: u}])

或者您可以通过选项:强制使用Repo.preload函数,但您还需要说明需要预先加载用户以进行评论关联。

修改

递归示例。

这是你的Post模块和结构

defmodule Post do
  schema "posts" do
    field :post_text, :string
    has_many :comments, Comment, foreign_key: :post_id
  end


  @doc """
    Recursively loads children into the given struct until it hits []
  """
  def load_comments(model), do: load_comments(model, 10)

  def load_comments(_, limit) when limit < 0, do: raise "Recursion limit reached"

  def load_comments(%Post{comments: %Ecto.Association.NotLoaded{}} = model, limit) do
    model 
        |> Repo.preload(:comments) # maybe include a custom query here to preserve some order
        |> Map.update!(model, :comments, fn(list) -> 
            Enum.map(list, fn(c) -> c |> Comment.load_parents(limit - 1) |> Comment.load_children(limit-1) end)
           end)
  end
end

这是你的评论模块和结构。

defmodule Comment do
  schema "comments" do
    belongs_to :parent, Test.Comment
    belongs_to :user, Test.User
    belongs_to :post, Test.Post
    has_many :children, Test.Comment, foreign_key: :parent_id
  end

  @doc """
    Recursively loads parents into the given struct until it hits nil
  """
  def load_parents(parent) do
    load_parents(parent, 10)
  end

  def load_parents(_, limit) when limit < 0, do: raise "Recursion limit reached"

  def load_parents(%Model{parent: nil} = parent, _), do: parent

  def load_parents(%Model{parent: %Ecto.Association.NotLoaded{}} = parent, limit) do
    parent = parent |> Repo.preload(:parent)
    Map.update!(parent, :parent, &Model.load_parents(&1, limit - 1))
  end

  def load_parents(nil, _), do: nil

  @doc """
    Recursively loads children into the given struct until it hits []
  """
  def load_children(model), do: load_children(model, 10)

  def load_children(_, limit) when limit < 0, do: raise "Recursion limit reached"

  def load_children(%Model{children: %Ecto.Association.NotLoaded{}} = model, limit) do
    model = model |> Repo.preload(:children) # maybe include a custom query here to preserve some order
    Map.update!(model, :children, fn(list) -> 
      Enum.map(list, &Model.load_children(&1, limit - 1))
    end)
  end
end

然后在控制器中

defmodule PostController do
  def show(id) do
    model = Repo.get(Post, id) 
      |> Post.load_comments

    # rendering, etc...
  end
end

答案 1 :(得分:0)

使用纯Ecto,我认为这是一个挑战,显示递归嵌套注释。您可以使用片段开发混合,如本答案https://stackoverflow.com/a/39400698/8508536

所示

这是一个使用Elixir中的原始postgres的示例查询,简化了类似于我所做的事情:

qry = "
  WITH RECURSIVE posts_r(id, posterid, parentid, parenttype, body, hash, depth) AS (
        SELECT p.id, p.posterid, p.parentid, p.parenttype, body, age, hash, 1
        FROM posts p
        WHERE p.parentid = " <> post_id <> " AND p.parenttype != 'room'
      UNION ALL
        SELECT p.id, p.posterid, p.parentid, p.parenttype, p.body, p.age, p.hash, pr.depth + 1
        FROM posts p, posts_r pr
      WHERE p.parentid = pr.id AND p.parenttype != 'room'
  )
  SELECT psr.id, psr.parentid, psr.parenttype, psr.body, psr.hash, psr.depth, u.name
  FROM posts_r psr LEFT JOIN users u ON psr.posterid = u.id
"

res = Ecto.Adapters.SQL.query!(Repo, qry, [])

cols = Enum.map res.columns, &(String.to_atom(&1))

comments = Enum.map res.rows, fn(row) ->
  struct(Comment, Enum.zip(cols, row))
end

comments