# 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>
```