2

I have the following in my Comment model (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

During migrations I had added:

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

I wanted to show the blog post, as well the comments and the replies on the comments (nested comments like reddit) on the post show page.

The comments and the nested comments are getting created correctly, but I am not able to resolve displaying the nested comments with the 'user' preloaded on the post/show page.

So in the post_controller, show function I have this:

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

In the _comment.html.eex, the following line I placed throws the error #Ecto.Association.NotLoaded<association :user is not loaded>:

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

Any help with this will be appreciated.

Update 1:

Output of running post = Post |> Repo.get(48) |> Repo.preload(comments: from(c in Comment, order_by: [desc: c.votes_up]), comments: :user, comments: :parent, comments: :children) as a sample post with comments and reply to comments.

%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", ...

Update 2:

In the post/show template this is what I have to display the comments:

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

And then in the _comment.html partial I do the following to display the parent comment and its nested children comments:

<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 %>
codingbear
  • 296
  • 5
  • 18
  • 1
    Can you post the queries that were executed by that preload? I ran a similar query and `post |> Map.get(:comments) |> Enum.at(0) |> Map.get(:user)` returns a `%User{}` for me. – Dogbert Aug 04 '17 at 04:57
  • Hi @Dogbert I added to the update the query result from running `post = Post |> Repo.get(48) |> Repo.preload(comments: from(c in Comment, order_by: [desc: c. inserted_at]), comments: :user, comments: :parent, comments: :children)` in the iex, which is the exact same as in the post controller – codingbear Aug 04 '17 at 05:35
  • That result does contain a value at `Enum.at(post.comments, 0).user.username` as far as I can see. Is that not the value you're trying to access in the template? What is `@comment` in the template? – Dogbert Aug 04 '17 at 05:39
  • Hey @Dogbert, I added in my second update whats in the post/show template and the _comment.html template to answer your question regarding `@comment` – codingbear Aug 04 '17 at 05:51

2 Answers2

1

From docs

Repo.preload preloads all associations on the given struct or structs.

This is similar to Ecto.Query.preload/3 except it allows you to preload structs after they have been fetched from the database.

In case the association was already loaded, preload won’t attempt to reload it.

Your better option is to use Query.preload since you are doing anyways everything in same pipe.

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}])

Or you can pass option :force in Repo.preload function but you need to also say tahat you need to preload user for comments association.

EDIT

Example with recursion.

Here is your Post module and struct

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

Here is your Comment module and struct.

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

Then in controller

defmodule PostController do
  def show(id) do
    model = Repo.get(Post, id) 
      |> Post.load_comments
      
    # rendering, etc...
  end
end
Community
  • 1
  • 1
Milan Jaric
  • 5,556
  • 2
  • 26
  • 34
  • I am sorry I didnt see your answer sooner, since I didnt get back to SO to check for updates. I tried with your answer, with the force option `Repo.preload([comments: from(c in Comment, order_by: [desc: c. inserted_at]), comments: :user, comments: :parent, comments: :children], force: true)` but I still get the error `#Ecto.Association.NotLoaded`. – codingbear Aug 08 '17 at 19:54
  • Also I tried the Query.preload option directly with the comments `query = Comment |> where([c], c.post_id == ^post.id) |> preload(:user) |> preload(parent: :user) |> preload(children: :user)` Next line: `comments = Repo.all(query)` and I get the error `#Ecto.Association.NotLoaded` – codingbear Aug 08 '17 at 19:54
  • So it is an issue with users association the if you want all to be fetch in single query use "join: u in assoc(c, :user)" and then "preload: [ :comment, {:c, user: u}]" and you will need join: c in assoc(p, :comments) above user join. I will update answer so it reflect this comment. Btw if you have more nested entities you have to do same for all of them – Milan Jaric Aug 12 '17 at 15:40
  • Hi @Milan Jaric, I really appreciate that you keep hitting up on updating on the open/unsolved question. The issue is that the comments have nested comments, and the nested comments have their own associations that are not getting preloaded. The updated answer that you gave was already a version that I tried in my own efforts to get it to work, but with no avail. – codingbear Aug 15 '17 at 04:27
  • Got it, then you need to use recursuion with tracking how deep you are going with preload since it could take long to get all data if replies nested very deeply. This could be one option https://gist.github.com/narrowtux/025da9ccea503ea7412664cc8a5a4dbdand. – Milan Jaric Aug 16 '17 at 06:13
  • Hey @Milan Jaric, yes recursion was also one of things I had thought of, and thought that there would have been example for that to learn from. The link that you gave yields a 404 error. – codingbear Aug 18 '17 at 04:59
  • I've updated answer but haven't tested it. Should be something like that. But that is very expensive way of loading data hierarchy. you should create additional field which should tell what is the "level" of hierarchy in comment and use that as filter. Later in view, just leave link to load additional replies for particular comment, level by level. – Milan Jaric Aug 18 '17 at 12:36
  • Wow @Milan Jaric, thanks so much for writing such a detailed update, and again I didnt come back to SO for a few days. So I made the changes to the post and comment models, as well as to the post controller, and I receive an error `function Map.update!/4 is undefined or private` just pointing to the line ` |> Post.load_comments`. I checked and re-checked line by line, but can't seem to find the culprit. And lack of detail even in the terminal does not help. – codingbear Aug 22 '17 at 17:56
  • I'm not sure in which line it fails, since in example I wrote it is update! with arrity 3. – Milan Jaric Nov 13 '17 at 15:39
0

With pure Ecto it is a challenge I think for showing recursive nested comments. You may be able to develop a hybrid using fragment as shown in this answer https://stackoverflow.com/a/39400698/8508536

Here is an example query using raw postgres in Elixir simplified from something similar I did:

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
  • Hi @swiftsubetei, thank you for stopping by and giving an answer. I am not familiar with raw sql/postgres commands; coming from rails have been pampered. For me to able to understand what happened in the code, I need to be able to understand each command like UNION, AND. I am sure its not hard, but just wanted ecto to be able to handle that level stuff for me. Over the weekend perhaps I will find time to brush up quickly on these commands and give it a shot in resolving this issue. – codingbear Aug 25 '17 at 23:04
  • no worries friend. I realized after it doesn't quite fit as an answer for the issue you wish to resolve. If I get a chance to try some code out I'll post an answer again as well if it works. Cheers good luck – swiftsubetei Aug 26 '17 at 00:05