# 1130 CRUD 文章留言功能 首先建立留言comment的model,需要內文,作者,所屬文章 `$ rails g model Comment content:text user:belongs_to post:belongs_to` 成功建立之後記得,`$ rails db:migrate`確認ok後 到comment.rb確認關聯 ``` belongs_to :user belongs_to :post ``` 另外在user.rb與post.rb也要增加關聯`has_many :comments`記得加s 這樣才完成comment的全部關聯性,has_many與belong_to不一定要成對,但少寫就會失去從另一邊搜尋回來的功能 接著在post的show.html.erb,因為留言要在文章下所以放在show頁面 ``` <%= form_for(@comment) do |f| %> .... <% end %> ``` 所以要在postcontroller建立show action給予@comment,這裡其實跟post的new action是一樣的只是改成用show action ``` def show @comment = Comment.new end ``` 現在回到瀏覽器會有缺少路徑的錯誤 在route.rb新增路徑,只給留言新增和刪除的功能 ``` resources :posts, only:[] do resources :comments, shallow: true, only: [:create, :destroy] end ``` 路徑建立後,因為不是原始CRUD,所以必須手動給予路徑post_comments_path,並且在新增留言時需要知道是在哪篇文章底下,就要給一個@post來確認post的id ``` <%= form_for(@comment, url: post_comments_path(@post)) do |f| %> <%= f.text_area :content, placeholder: '請輸入留言' %> <%= f.submit '送出' %> <% end %> ``` placeholder可以有產生預設字樣,在輸入之後會消失 在comment.rb建立留言的驗證,必須要有輸入才可以送出,不可有空留言 ` validates :content, presence: true` 現在到瀏覽器輸入留言按下送出會出現缺少CommentsController的錯誤 可以用終端機指令建立但會多一些用不到的檔案,這裡我們用手動建立CommentsController ``` class CommentsController < ApplicationController before_action :session_required //留言前確認有登入 def create end end ``` 新增留言其實與新增文章相似,所以也需要清理資料,同樣在CommentsController裡,定義一個comment_params ``` private def comment_params params.require(:comment).permit(:content) end ``` 現在就可以寫完整create action ``` class CommentsController < ApplicationController before_action :session_required def create @post = Post.find(params[:post_id]) //找出要留言的文章 @comment = current_user.comments.new(comment_params) @comment.post = @post //將@post指定為這篇留言的post if @comment.save redirect_to @post, notice: '留言已新增' else render 'posts/show' end end private def comment_params params.require(:comment).permit(:content) end end ``` 另外`@post = Post.find(params[:post_id])`也可以整理成 ` before_action :find_post, only: [:create]`的形式 在private增加 ``` def find_post @post = Post.find(params[:post_id]) end ``` 這個整理雖然只有create action用到,只是為了維持慣例才改寫 到這裡完成新增留言的功能 --- 再來是將已經新增的留言在show頁面呈現出來 先在PostsController的show將全部的comment全部找出來 ``` def show @comment = Comment.new @comments = @post.comments //文章的所有留言,comments是has_many :comments關聯性產的的方法 //@comments的s是慣例寫法代表全部comment end ``` 在post的show頁面,增加文章列表 ``` <ul> <% @comments.each do |comment| %> <li> <% comment.content %> [<% comment.user.nickname %>] //留言作者暱稱 </li> <% end %> </ul> ``` 但是現在的列表新留言會出現在最下面,用order重新排序 `@comments = @post.comments.order(id: :desc)` ## 出現n+1問題(重要必考) ``` <li> <% comment.content %> [<% comment.user.nickname %>] //留言作者暱稱 </li> ``` 會出現n+1問題是因為一開始show action只有撈出comment的資料,而n是因為每轉一次迴圈就撈一次nickname而產生 解決方法可以在show action撈全部comment時一併處理user的資料,這樣就只會撈一次全部comment,還有一次撈comment的user資料一共兩次,在轉迴圈時就已經有全部comment與user的資料了 ``` def show @comment = Comment.new @comments = @post.comments.order(id: :desc).includes(:user) end ``` --- ### 刪除留言功能 ``` <ul> <% @comments.each do |comment| %> <li> <% comment.content %> [<% comment.user.nickname %>] //留言作者暱稱 <%= link_to '刪除', comment,method: 'deleted' ,data:{ confirm: '確認?'}%> </li> <% end %> </ul> ``` * 路徑的comment是comment_path(comment)的縮寫 * comment_path從查表得知 接下來會有缺少destroy action的錯誤 在CommentsController ``` def destroy comment = current_user.comments.find(params[:id]) //從使用者角度找留言,找到指定的那一篇文章 //select * from comments where comment_id = ? and user_id = ? comment.destroy redirect_to comment.post, notice: '留言已刪除' end ``` 如果用下面的寫法,會可以刪除任意找到id的留言,無視是否為留言者 `comment = Comment.find(params[:id])` --- ### 軟刪除留言(soft delete) 先在comment新增一個deleted_at欄位,預設值為nil `$ rails g migration add_deleted_at_to_comment` 在db資料夾會出現一個新的檔案:日期_add_deleted_at_to_comment ``` class AddDeletedAtToComment < ActiveRecord::Migration[6.0] def change add_column :comments, :deleted_at, :datetime, default: nil add_index :comments, :deleted_at //建立索引,減少資料庫搜尋時間 end end ``` 再進行 `$ rails db:migrate`就完成欄位的新增 再到CommentsController修改destroy action,將原本的刪除行為改成更新deleted_at為現在時間 ``` def destroy comment = current_user.comments.find(params[:id]) # comment.destroy comment.update(deleted_at: Time.now) redirect_to comment.post, notice: '留言已刪除' end ``` 再到PostsController修改show action,讓原本撈全部comment改成只撈deleted_at值為nil的(沒有執行過刪除功能的comment) ``` def show @comment = Comment.new @comments = @post.comments.where(deleted_at: nil).order(id: :desc).includes(:user) end ``` --- ## 新增留言的效果演出 先給予留言列表一個class名稱以便後面使用 ``` <ul class = "comment"> <% @comments.each do |comment| %> <li> <% comment.content %> [<% comment.user.nickname %>] //留言作者暱稱 <%= link_to '刪除', comment,method: 'deleted' ,data:{ confirm: '確認?'}%> </li> <% end %> </ul> ``` 在views資料夾下建立/comment/create.js.erb,寫下js code ``` var comment_area = document.querySelector('.comment'); //選取整個ul comment_area.innerHTML = "<li>[122] <%= @comment.content %> <a herf='#'>刪除</a></li>" + comment_area.innerHTML; ``` 會再送出後執行此js code,用`<li>[122] <%= @comment.content %> <a herf='#'>刪除</a></li>`模擬類似文章已存入的顯示畫面,並且不會像原來新增留言時那樣跳轉頁面,但是目前的作者與刪除是假的,刪除沒有效果 將留言列表獨立成一個檔案 在views/comments建立_comment.html.erb寫入 ``` <li> <% comment.content %> [<% comment.user.nickname %>] <%= link_to '刪除', comment,method: 'deleted' ,data:{ confirm: '確認?'}%> </li> ``` 然後在原來的ul中li的位置改成 `<%= render "comments/commeny",comment: comment %>` create.js.erb可以再改成 ``` var comment_area = document.querySelector('.comment'); comment_area.innerHTML = "<%= j render('comments/comment', comment: @comment) %>" + comment_area.innerHTML; document.querySelector('#comment_content').value = '' //留言新增後,清除輸入框內文字 ``` 到此整個動作為在按下送出後,會執行create.js.erb,再用_comment.html.erb進行宣染,新輸入的留言會用@comment帶入`render('comments/comment', comment: @comment)`呈現,原有留言則是`comment_area.innerHTML`來呈現,這時候的作者與刪除就是來自li內,可以正常發揮功能 * j 是rails內建的,用來跳脫避免語法錯誤 * @comment是從creat action得來的,以當下輸入的那筆留言來宣染( comment: @comment) * _comment.html.erb中的li沒有用到@comment所以不需要修改(comment: comment) ### 但是在迴圈中做render是非常影響效能的 ``` <ul class = "comment"> <% @comments.each do |comment| %> <%= render "comments/commeny",comment: comment %> <% end %> </ul> ``` 可以用render collection改寫 ``` <ul class = "comment"> <% render @comments %> </ul> ```