【问题标题】:Elixir Phoenix - preload association with selfElixir Phoenix - 与自我的预加载关联
【发布时间】:2017-08-24 22:32:38
【问题描述】:

我的评论模型(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

我想在帖子显示页面上显示博客文章,以及 cmets 和对 cme​​ts 的回复(嵌套 cmets,如 reddit)。

cmets 和嵌套 cmets 已正确创建,但 我无法解决在发布/显示页面上预加载“用户”的情况下显示嵌套 cmets。

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

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 |&gt; Repo.get(48) |&gt; Repo.preload(comments: from(c in Comment, order_by: [desc: c.votes_up]), comments: :user, comments: :parent, comments: :children) 的输出作为与 cmets 的示例帖子并回复 cmets。

%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:

在 post/show 模板中,这是我必须显示 cmets 的内容:

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

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

<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 %>

【问题讨论】:

  • 您可以发布该预加载执行的查询吗?我运行了一个类似的查询,post |&gt; Map.get(:comments) |&gt; Enum.at(0) |&gt; Map.get(:user) 为我返回了一个%User{}。
  • 嗨 @Dogbert 我在更新中添加了在 iex 中运行 post = Post |&gt; Repo.get(48) |&gt; Repo.preload(comments: from(c in Comment, order_by: [desc: c. inserted_at]), comments: :user, comments: :parent, comments: :children) 的查询结果,这与后控制器中的完全相同
  • 据我所知,该结果确实包含Enum.at(post.comments, 0).user.username 的值。这不是您要在模板中访问的值吗?模板中的@comment 是什么?
  • 嘿@Dogbert,我在第二次更新中添加了帖子/节目模板和_comment.html 模板中的内容,以回答您关于@comment的问题

标签: elixir phoenix-framework


【解决方案1】:

来自文档

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

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

如果关联已经加载,preload 不会尝试重新加载它。

您更好的选择是使用 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 函数中传递选项 :force 但你还需要说你需要为 cmets 关联预加载用户。

编辑

递归示例。

这是你的 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

【讨论】:

  • 很抱歉我没有早点看到你的答案,因为我没有回到 SO 检查更新。我尝试了你的答案,使用强制选项Repo.preload([comments: from(c in Comment, order_by: [desc: c. inserted_at]), comments: :user, comments: :parent, comments: :children], force: true),但我仍然收到错误#Ecto.Association.NotLoaded&lt;association :user is not loaded&gt;。
  • 我还直接使用 cmets query = Comment |&gt; where([c], c.post_id == ^post.id) |&gt; preload(:user) |&gt; preload(parent: :user) |&gt; preload(children: :user) 尝试了 Query.preload 选项 下一行:comments = Repo.all(query) 我收到错误 #Ecto.Association.NotLoaded&lt;association :children is not loaded&gt;
  • 所以这是用户关联的问题,如果您希望在单个查询中获取所有内容,请使用“join: u in assoc(c, :user)”然后“preload: [:comment, {:c, user: u}]" 并且您将需要 join: c in assoc(p, :cmets) 上面的用户加入。我将更新答案,以反映此评论。顺便说一句,如果你有更多的嵌套实体,你必须对它们都做同样的事情
  • 嗨@Milan Jaric,我非常感谢您不断更新未解决/未解决的问题。问题是 cmets 有嵌套的 cmets,而嵌套的 cmets 有自己的关联,这些关联没有被预加载。您提供的更新答案已经是我尝试使其工作的版本,但无济于事。
  • 知道了,那么您需要使用递归来跟踪预加载的深度,因为如果回复嵌套非常深,获取所有数据可能需要很长时间。这可能是一种选择gist.github.com/narrowtux/025da9ccea503ea7412664cc8a5a4dbdand。
【解决方案2】:

对于纯 Ecto,我认为显示递归嵌套 cmets 是一个挑战。您也许可以使用片段开发混合,如此答案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

【讨论】:

  • 嗨@swiftsubetei,感谢您的光临并给出答案。我不熟悉原始 sql/postgres 命令;来自铁轨的人已经被宠坏了。为了让我能够理解代码中发生的事情,我需要能够理解每个命令,例如 UNION 和 AND。我相信这并不难,但只是希望 ecto 能够为我处理那个级别的东西。周末也许我会抽出时间快速复习这些命令并尝试解决这个问题。
  • 别担心,朋友。我意识到它不太适合作为您希望解决的问题的答案。如果我有机会尝试一些代码,如果它有效,我也会再次发布答案。祝你好运
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-20
  • 2016-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-16
  • 1970-01-01
相关资源
最近更新 更多