嵌套评论线程——视图中的递归渲染

Nested comment thread - recursive rendering in view

我正在尝试在 Rails 6 中创建一个问答线程,用户可以在其中回答问题,然后其他用户可以对答案发表评论 - 类似于 reddit 甚至 Whosebug。

我用 'parent_id' 在我的 Answer 模型上创建了一个多态关联,我能够 post 回答初始问题。但是,嵌套答案不会呈现在初始答案下方,而是呈现在主要问题下方。我想我已经将问题隔离到下面看到的相应部分视图:

回答查看

<li>
<%= answer.body %></br>
<%= link_to answer.user.first_name, answer.user %> 
<%= link_to answer.user.last_name, answer.user %> 
answered <%= time_ago_in_words(answer.created_at) %> ago.

<div class="comments-container">
<%= render partial: "answers/reply", locals: {commentable: answer.commentable, parent_id: answer.parent.id} %>  
</div>

<ul> <%= render partial: "answers/answer", collection: answer.answers %> </ul>
 </li>

根据我的理解,最后一行应该呈现答案的答案,但答案呈现在初始问题下方,而不是答案下方。关于我做错了什么的任何想法? 我应该使用 Ancestry 之类的 gem 来执行此操作吗?如果是这样,那将如何工作?

为了完整起见,这里是其他组件

问题查看

<h3><%= @question.title %></h3>
<p> Asked by <%= link_to @question.user.email, @question.user %> <%= time_ago_in_words(@question.created_at) %> ago. </p>
</br>
<span class="body"> <%= @question.body %> </span>
</br>

<h5><strong><%= @question.answers.count %> Answers</strong></h5>

<%= render @answers %></br>
<%= render partial: "answers/form", locals: {commentable: @question} %> </br>
<%= paginate @answers %>

答案模型

belongs_to :user
belongs_to :parent, optional: true, class_name: 'Answer'
belongs_to :commentable, polymorphic: true
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true 
validates :user, presence: true

问题模型

belongs_to :user
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true
validates :title, presence: true
validates :user, presence: true

AnswerController

class AnswersController < ApplicationController
before_action :set_answer, only: [:edit, :update, :destroy, :upvote, :downvote]
before_action :find_commentable, only: [:create]

def new
  @answer = Answer.new
end

def create
  @answer = @commentable.answers.new(answer_params)
  respond_to do |format|
    if @answer.save
      format.html { redirect_to @commentable }
      format.json { render :show, status: :created, location: @commentable }
    else
      format.html { render :new }
      format.json { render json: @answer.errors, status: :unprocessable_entity }
    end
  end
end

def destroy
  @answer = @commentable.answers.find(params[:id])
  @answer.discard
  respond_to do |format|
    format.html { redirect_to @commentable, notice: 'Answer was successfully destroyed.' }
    format.json { head :no_content }
  end
end

private
def set_answer
  @answer = Answer.find(params[:id])
end

def answer_params
  params.require(:answer).permit(:body).merge(user_id: current_user.id, parent_id: params[:parent_id])
end

def find_commentable
  @commentable = Answer.find(params[:answer_id]) if params[:answer_id]
  @commentable = Question.find(params[:question_id]) if params[:question_id]
end

end

问题控制器

class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy, :upvote, :downvote]

def index
  @questions = Question.order('created_at desc').page(params[:page])
end

def show
  @answer = @question.answers.new  
  @answers = if params[:answer]
             @question.answers.where(id: params[:answer])
             else
             @question.answers.where(parent_id: nil)
             end

  @answers = @answers.page(params[:page]).per(5)
end

def new
  @question = Question.new
end

def edit
end

def create
  @question = Question.new(question_params)
  respond_to do |format|
    if @question.save
      format.html { redirect_to @question, notice: 'You have successfully asked a question!' }
      format.json { render :show, status: :created, location: @question }
    else
      format.html { render :new }
      format.json { render json: @question.errors, status: :unprocessable_entity }
    end
  end
end

def update
  respond_to do |format|
    if @question.update(question_params)
      format.html { redirect_to @question, notice: 'Question successfully updated.' }
      format.json { render :show, status: :ok, location: @question }
    else
      format.html { render :edit }
      format.json { render json: @question.errors, status: :unprocessable_entity }
    end
  end
end

def destroy
  @question.discard
  respond_to do |format|
    format.html { redirect_to @questions_url, notice: 'Question successfully deleted.' }
    format.json { head :no_content }
  end
end

private

def set_question
  @question = Question.find(params[:id])
end

def question_params
  params.require(:question).permit(:title, :body, :tag_list).merge(user_id: current_user.id)
end

end

你在多态性建模方面有点失败。如果你想要一个真正的多态关联,你可以这样建模:

class Question
  has_many :answers, as: :answerable
end

class Answer
  belongs_to :answerable, polymorphic: true 
  has_many :answers, as: :answerable
end

这让问题的 "parent" 成为问题或答案,您不需要做像 @question.answers.where(parent_id: nil) 这样荒谬的事情。你可以只做 @answers = @question.answers 这将只包括第一代 children.

然而,多态性并不像吹嘘的那样,在构建树层次结构时尤其如此。parent。由于我们实际上必须将行从数据库中拉出才能知道在哪里加入,因此您不能急于有效地加载树。如果 parent 类 的数量很大或未知,或者您只是在制作原型,多态性主要有用。

相反,您可以使用单一 Table 继承来设置关联:

class CreateAnswers < ActiveRecord::Migration[6.0]
  def change
    create_table :answers do |t|
      t.string :type
      t.belongs_to :question, null: true, foreign_key: true
      t.belongs_to :answer, null: true, foreign_key: true
      # ... more columns
      t.timestamps
    end
  end
end

注意可为空的外键列。与多态性不同,这些是真正的外键,因此数据库将确保参照完整性。另请注意 type 列,它在 ActiveRecord 中具有特殊意义。

然后让我们设置模型:

class Question < ApplicationRecord
  has_many :answers, class_name: 'Questions::Answer'
end

class Answer < ApplicationRecord
  has_many :answers, class_name: 'Answers::Answer'
end

以及答案的子类:

# app/models/answers/answer.rb
module Answer
  class Answer < ::Answer
    belongs_to :answer
    has_one :question, through: :answer
  end
end

# app/models/questions/answer.rb
module Questions
  class Answer < ::Answer
    belongs_to :question
  end
end

很酷。现在我们可以通过以下方式预先加载到第一代和第二代:

Question.eager_load(answers: :anser)

我们可以继续前进:

Question.eager_load(answers: { answers: :answer })
Question.eager_load(answers: { answers: { answers: :answers }})

但在某些时候你会想要退出并开始使用 ajax 就像 reddit 一样。