Как правильно использовать рекурсивную функцию в представлении рельсов? - PullRequest
0 голосов
/ 25 апреля 2019

Я создаю систему вложенных комментариев, похожую на reddit, где комментарии вложены под другим комментарием и имеют практически неограниченный уровень глубины.

Модель комментария использует самоссылающийся идентификатор.

У меня есть частичное представление, называемое комментарием, которое отображает один комментарий, и я пытаюсь использовать рекурсивную функцию для вычленения каждого комментария один за другим.

вид

<% comments.where(parent_id: nil).each do |parent| %>
  <!-- render root node -->
  <%= render partial: "comment", locals: { comment: parent } %>
  <!-- recursively render child nodes -->
  <%= render_children(parent.id) %>
<% end %>

1010 * вспомогательный *

def render_children(id)
  Comment.where(parent_id: id).each do |comment|
    render partial: "comment", locals: { comment: comment }
    render_children(comment.id)
  end 
end

Это не работает, потому что помощники не могут вызывать рендеринг более одного раза, я также пытался определить функцию, на мой взгляд, но это тоже не похоже.

Мне интересно, подхожу ли я к этой проблеме неправильно.

Как правильно использовать рекурсивные функции, чтобы я мог рендерить древовидную структуру в моем представлении rails?

Ответы [ 4 ]

1 голос
/ 25 апреля 2019

Помощник может отображать более одного раза, но он должен объединить полученные строки и вернуть только одну:

def render_children(id)
  children = Comment.where(parent_id: id).to_a
  safe_join(
    children.map{|comment|
      safe_join([
        render(partial: "comment", locals: { comment: comment }),
        render_children(comment.id)
      ])
    }
  )
end

Выполнение отдельного запроса для каждого комментария создаст слишком большую нагрузку для больших потоков.

0 голосов
/ 26 апреля 2019

Этот код является ярким примером N + 1 запроса , который снизит производительность вашего приложения. Каждая итерация вызывает Comment.where(parent_id: id), что создает дополнительный запрос к базе данных.

Вы должны начать с установки надлежащих ассоциаций, чтобы вы могли просто вызвать #children для экземпляра комментария, чтобы получить вложенные комментарии, вместо того, чтобы делать Comment.where(parent_id: id):

class Comment
  belongs_to :parent, class_name: 'Comment', optional: true
  has_many :children, class_name: 'Comment', foreign_key: :parent_id
end

Это позволит вам использовать .includes или .eager_load для извлечения дочерних элементов в одном запросе:

<% render partial: 'comment', collection: Comment.where(parent_id: nil).includes(:children) %>

Однако это будет работать только на один уровень. Rails на самом деле не поддерживает рекурсивную загрузку ассоциаций, но вы можете подделать ее:

class Comment < ApplicationRecord
  belongs_to :parent, class_name: 'Comment', optional: true
  has_many :children, class_name: 'Comment', foreign_key: :parent_id

  def self.deep_includes(levels = 5)
    hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc)  }
    keys = Array.new(levels, :children)
    keys.inject(hash) {|h, k| h[k] }[:children] = :children  
    self.includes(hash)
  end
end

Это действительно так:

Comment.includes({:children=>{:children=>{:children=>{:children=>{:children=>{:children=>:children}}}}}})

, который создает колоссальный SQL-запрос, объединяющий каждый уровень.

<% render partial: 'comment', collection: Comment.where(parent_id: nil).deep_includes %>

С этим позаботиться о создании рекурсивного частичного на самом деле намного проще, чем вы думаете:

# app/views/comments/_comment.html.erb
<div class="comment">
   <p><%= comment.data %></p>
   <% if comment.children.any? %>
   <div class="children">
     <%= render partial: "comment", collection: comment.children %>
   </div>
   <% end %>
</div>

Вам действительно не нужен вспомогательный метод и дополнительный уровень сложности.

0 голосов
/ 25 апреля 2019

На мой взгляд, это самый простой способ рекурсивного рендеринга компонентов в Rails:

Внутри частичного _comment.html.erb

<%= comment.data %>
<% comment.children.each do |child| %>
  <ul class="child-thread">
    <%= render partial: 'comments/comment', locals: { comment: child } %>
  </ul>
<% end %>

Ваша Comment модель должна иметь этот метод:

class Comment < ApplicationRecord
  has_one :parent, :foreign_key => :parent_id
  def children 
    Comment.where(parent_id: self.id)
  end
end

Итак, комментарий имеет parent_id, который либо равен NULL (в случае комментария корневого уровня), либо это другой comment.id. Вы можете легко установить жесткую блокировку на уровне рекурсии, передав локальную переменную в строку:

<%= comment.data %>
<% if count < 5 %>
  <% comment.children.each do |child| %>
    <ul class="child-thread">
      <%= render partial: 'comments/comment', 
          locals: { comment: child, count: count + 1 } %>
    </ul>
  <% end %>
<% end %>
0 голосов
/ 25 апреля 2019

Я считаю, что лучший способ справиться с этим - это перебрать вашу дочернюю коллекцию и визуализировать частичную для каждого в вашем представлении:

<% comments.where(parent_id: nil).each do |parent| %>
  <!-- render root node -->
  <%= render partial: "comment", locals: { comment: parent } %>
  <% parent.children.each do |child| %>
    <%= render partial: "comment", locals: { comment: child } %>
  <% end %>
<% end %>

Обратите внимание, это предполагает, что ваш родительский объект имеет множество children (замените его на любое отношение, которое не соответствует действительности), и что вы хотите отобразить один и тот же фрагмент с именем comment как для родителя, так и для детей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...