【Ruby on Rails】CRUD操作とモデルの関連付けとBASIC認証の設定

初回で、[Ruby on Railsを全くゼロの状態からプロジェクトを作成し、DBから取得したデータをブラウザに表示](https://qiita.com/yuta-38/items/e7bb2584d3421ce11e5a)まで実装し、

次に、[アプリケーションにCRUD（作成、読み取り、更新、削除）を実装](https://qiita.com/yuta-38/items/6cde2ccde5495ec16a6c)まで行った。

今回は、その続きで２つ目のモデルを作成し関連付けを行う。また、BASIC認証を設定し、一部のページへはログインユーザーしかアクセスできないようにする。

https://railsguides.jp/getting_started.html


##目次

1. [Commentモデルの生成](#1-commentモデルの生成)
2. [マイグレーション](#2-マイグレーション)
3. [Articleモデルの関連付け処理](#3-articleモデルの関連付け処理)
4. [ルーティングの追加](#4-ルーティングの追加)
5. [コントローラの生成と編集](#5-コントローラの生成と編集)
6. [ビューにコメント入力欄を追加](#6-ビューにコメント入力欄を追加)
7. [ビューにコメントの表示欄を追加](#7-ビューにコメントの表示欄を追加)
8. [パーシャルを使ったコメントのリファクタリング](#8-パーシャルを使ったコメントのリファクタリング)
9. [パーシャルを使ったコメントフォームのリファクタリング](#9-パーシャルを使ったコメントフォームのリファクタリング)
10. [コメントの削除機能](#10-コメントの削除機能)
11. [コントローラにdestroyアクションを追加](#11-コントローラにdestroyアクションを追加)
12. [関連づけられたオブジェクトの削除処理](#12-関連づけられたオブジェクトの削除処理)
13. [BASIC認証の実装](#13-basic認証の実装)



<br>
##1. Commentモデルの生成
Articleモデルに続いて２つ目のモデルCommentを生成する。

```
$ bin/rails generate model Comment commenter:string body:text article:references
```

・`モデル名:references`
このオプションをつけることで、生成するCommentモデルがArticleモデルに従属（belongs_to）することを指定できる。


**▼実行結果**

```
$ rails generate model Comment commenter:string body:text article:references

Running via Spring preloader in process 48727
      invoke  active_record
      create    db/migrate/20210310111213_create_comments.rb
      create    app/models/comment.rb
      invoke    test_unit
      create      test/models/comment_test.rb
      create      test/fixtures/comments.yml
```


４つのファイルが生成される。重要なのは、モデルファイル`comment.rb`。中身は以下。


```rb:comment.rb（生成されたモデルファイルの中身）
class Comment < ApplicationRecord
  belongs_to :article
end
```

関連付けを示すbelongs_toが記述されている。


<br>
##2. マイグレーション
生成されたマイグレーションファイルを元に、マイグレーションを行いDBにテーブルを作成する。

```
$ rails db:migrate
```


<br>
関連付けはこれだけでは不十分で、関連付け先のArticleモデルでも追記する必要がある。


<br>
##3. Articleモデルの関連付け処理
app/models/article.rb

```rb:article.rb
class Article < ApplicationRecord
  has_many :comments
  validates :title, presence: true,
                    length: { minimum: 5 }
end
```

関連付けの部分は`has_many :comments`。


###関連付けのメリット
親となるインスタンスで、`.従属するモデル名`（小文字）を指定すると、関連付けられたモデルのデータも引っ張ってくることができる。

```rb:例
@article = Article.find([:id])  #指定したidの記事データを取得

#関連付けたモデルのデータを取得
comments = @article.comments #その記事に関連するコメントをすべて取得
```


###has_manyとは？
１対多の関連付けを行う場合は`has_many`ヘルパーを使う。複数のインスタンスと紐づけるため、関連づけるモデル名は複数形にする。

####・1対多とは？
今回のように、記事という一つのインスタンスに対して、たくさんの人が口コミをするため、複数のインスタンスが紐づくようになる。これが1対多の関係。


####・１対１とは？
１対１の関係とは、例えば、Articleに対して、著者Authorのように１つのインスタンスが関連する場合をいう。



<br>
##4. ルーティングの追加
CommentモデルもCRUD操作を行うため、`resources`ヘルパーでリソースフルなルーティングを追加する。

articlesに**ネストしたリソース**としてcommentsを作成する。

```rb:routes.rb
Rails.application.routes.draw do
  root "articles#index"
  resources :articles

  #追記
  resources :articles do
    resources :comments
  end
end
```



<br>
**▼ルーティングの確認**

```
$ bin/rails routes
                                  Prefix Verb   URI Pattern                                                                                       
                        article_comments GET    /articles/:article_id/comments(.:format)                                                          comments#index
                                         POST   /articles/:article_id/comments(.:format)                                                          comments#create
                     new_article_comment GET    /articles/:article_id/comments/new(.:format)                                                      comments#new
                    edit_article_comment GET    /articles/:article_id/comments/:id/edit(.:format)                                                 comments#edit
                         article_comment GET    /articles/:article_id/comments/:id(.:format)                                                      comments#show
                                         PATCH  /articles/:article_id/comments/:id(.:format)                                                      comments#update
                                         PUT    /articles/:article_id/comments/:id(.:format)                                                      comments#update
                                         DELETE /articles/:article_id/comments/:id(.:format)                                                      comments#destroy
                                         GET    /articles(.:format)                                                                               articles#index
                                         POST   /articles(.:format)                                                                               articles#create
                                         GET    /articles/new(.:format)                                                                           articles#new
                                         GET    /articles/:id/edit(.:format)                                                                      articles#edit
                                         GET    /articles/:id(.:format)                                                                           articles#show
                                         PATCH  /articles/:id(.:format)                                                                           articles#update
                                         PUT    /articles/:id(.:format)                                                                           articles#update
                                         DELETE /articles/:id(.:format)                                                                           articles#destroy
```

/articles/:article_id/commentsなど、article配下にcommentsを含むルートが生成されている。



<br>
##5. コントローラの生成と編集
###5-1. コントローラの生成

```
$ rails generate controller Comments
```

<br>
**▼実行例**

```
$ rails generate controller Comments

Running via Spring preloader in process 53907
      create  app/controllers/comments_controller.rb
      invoke  erb
      create    app/views/comments
      invoke  test_unit
      create    test/controllers/comments_controller_test.rb
      invoke  helper
      create    app/helpers/comments_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/comments.scss
```

４つのファイルと１つの空ディレクトリが生成される。


###5-2. コントローラの編集
commentsコントローラーに口コミを生成するcreateアクションを追加する。

app/controllers/comments_controller.rb


```rb:comments_controller.rb
class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  #ストロングパラメータ
  private
    def comment_params
      params.require(:comment).permit(:commenter, :body)
    end
end
```

・`@article = Article.find(params[:article_id])`
Articleモデルで、パラメータで受け取ったIDに該当するデータをインスタンス変数`@article`に代入する。

・`@article.comments.create(comment_params)`
記事に該当するコメント（Commentモデル）のデータを作成と保存を同時に行う。引数にはストロングパラーメタを渡す。メソッドの戻り値が入力値となる。

.createは作成と保存。作成のみにしたい場合は.buildを使う。


・ストロングパラメータとは？
悪意のあるユーザーがDBのデータ変更をしないように、使用できるカラムを制限するプライベートメソッド。


・`redirect_to article_path(@article)`
.create処理が完了したら、新しいセッションで元の記事（@article）に戻る。

このヘルパーにより、ArticlesControllerのshowアクションが呼び出され、show.html.erbがレンダリングされる。



<br>
##6. ビューにコメント入力欄を追加
Articleコントローラのshowアクションのテンプレート（showテンプレート）にcommentのフォームを追加する。。

app/views/articles/show.html.erb

```erb:
<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>

<p>
  <strong>Text:</strong>
  <%= @article.body %>
</p>

<h2>Add a comment:</h2>
<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

<%= link_to 'Back', articles_path %>
| <%= link_to 'Edit', edit_article_path(@article) %>
```

**・`form_with model: モデルクラスのインスタンス do |form|`**
モデルクラスのインスタンスに、`@article`と`@article.comments.build`の２つを渡している。


<br>
**・`local: true`**
HTML形式でリクエスト送信することができる。（data-remote="true"を無くす）


<br>
**▼ブラウザの表示**
http://localhost:3000/articles/1/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/c81af505-06fa-7264-0be3-673db398fe4d.png)



<br>
##7. ビューにコメントの表示欄を追加
追加されたコメントを表示する場所を作成するため、以下を追記する。

app/views/articles/show.html.erb

```erb:show.html.erb
<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>
```

すべてのコメント`@article.comments`から`each`メソッドでコメントを一つづつ取り出して表示する。


**▼ブラウザの表示**
http://localhost:3000/articles/2

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/4825bcb2-7984-87a0-044a-62b56199aca1.png)

入力したコメントが表示された。

<br>
##8. パーシャルを使ったコメントのリファクタリング
パーシャル（部分テンプレート）を使ってshow.html.erbをシンプルにする。

###8-1. コメント表示を切り出す

app/views/comments/_comment.html.erbを作成し、以下を記述する。

パーシャルであることを明示するためにファイル名の冒頭に`_`をつける。

```erb:_comment.html.erb
<p>
  <strong>Commenter:</strong>
  <%= comment.commenter %>
</p>

<p>
  <strong>Comment:</strong>
  <%= comment.body %>
</p>
```


###8-2. パーシャルを読み込む
app/views/articles/show.html.erbで、コメントを表示していたコードを置き換える。

```erb:show.html.erb
<%= render @article.comments %>
```

renderにインスタンスの複数データ（comments）を渡すと、.eachメソッドを自動で実行して、commentという変数名で指定のテンプレートに渡す。


```:例１（複数形）
render @article.comments

↓

render partial: 'comments/comment', collection: @article.comments
```

パーシャルの中で、変数commentが使える。（comments.eachの実行結果）


<br>

```:例２（単数形）
render @article

↓

render partial: 'article', locals: { article: @article }

↓

render 'article', article: @article
```

パーシャルの中で、変数articleが使える。


<br>
##9. パーシャルを使ったコメントフォームのリファクタリング
次にコメントのフォーム部分もリファクタリングする。

###9-1. コメントフォーム表示を切り出す

app/views/comments/_form.html.erbファイルを作成し、show.index.htmlのフォーム部分をコピペする。

```erb:_form.html.erb
<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>
```

<br>
###9-2. パーシャルを読み込む
app/views/articles/show.html.erbで、コメントを表示していたコードを置き換える。

```erb:show.html.erb
<%= render "comments/form" %>
```

renderで階層をそのまま指定している。`app/view/配下の/comments/ディレクトリの`_form.html.erb`を参照している事になる。

```:例
render "comments/form"

↓ 同じ

render "app/views/comments/_form.html.erb"
```


<br>
##10. コメントの削除機能
CommentsControllerにdestroyアクションを作成する。

app/views/comments/_comment.html.erb　に削除用リンクを追加する。


```erb:_comment.html.erb
<p>
  <%= button_to 'Destroy Comment', [comment.article, comment],
               method: :delete,
               data: { confirm: 'Are you sure?' } %>
</p>
```

アンカーテキスト Destroy Comment をクリックすると、`DELETE /articles/:article_id/comments/:id`というリクエストがCommentsControllerに送信される。

・`link_to 'アンカーテキスト', ルートパス, オプション`


```:（参考）ルート
article_comment DELETE /articles/:article_id/comments/:id(.:format) comments#destro
```



<br>
##11. コントローラにdestroyアクションを追加
app/controllers/comments_controller.rb

```rb:comments_controller.rb
  def destroy
    @article = Article.find(params[:article_id])
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    redirect_to article_path(@article)
  end
```

対象の記事、対象のコメントを指定して、destroyメソッドで削除する。

削除後は、redirect_toでヘルパーでpathヘルパーを指定し、対象の記事ページに遷移する。



<br>
##12. 関連づけられたオブジェクトの削除処理
ある記事を削除したら、その記事に関連付けられているコメントも一緒に削除する処理を作成する。これをしないとDB上にデータが残ってしまう。

削除処理を関連づけるため、モデルに`dependent: :destroy`を追記する。

app/models/article.rb

```rb:article.rb
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
  validates :title, presence: true,
                    length: { minimum: 5 }
end
```


###確認
http://localhost:3000/articles/8

記事の詳細ページでコメントを追加する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/72505cf7-417a-30c0-5666-3b73c3fe94f3.png)


```
$ bin/rails c

#1つ目のコメント
2.7.0 :019 > Comment.find(8)
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."id" = ? LIMIT ?  [["id", 8], ["LIMIT", 1]]
 => #<Comment id: 8, commenter: "１つ目のコメント", body: "１つ目のコメントです。", article_id: 11, created_at: "2021-03-11 05:52:23.144922000 +0000", updated_at: "2021-03-11 05:52:23.144922000 +0000">


#2つ目のコメント
2.7.0 :019 > Comment.find(9)
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."id" = ? LIMIT ?  [["id", 9], ["LIMIT", 1]]
 => #<Comment id: 9, commenter: "２つ目のコメント", body: "２つ目のコメントです。", article_id: 11, created_at: "2021-03-11 05:55:25.893754000 +0000", updated_at: "2021-03-11 05:55:25.893754000 +0000">  
```

Commentモデル経由でデータを取得すると、作成したコメントがDBに保存していることがわかる。

↓　2つ目のコメントを削除

```
2.7.0 :022 > Comment.find(9)
  Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."id" = ? LIMIT ?  [["id", 9], ["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):22
ActiveRecord::RecordNotFound (Couldn't find Comment with 'id'=9)
```

DBから削除できている。

↓ 記事を削除

```
2.7.0 :023 > Comment.find(8)
  Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."id" = ? LIMIT ?  [["id", 8], ["LIMIT", 1]]
Traceback (most recent call last):
        2: from (irb):22
        1: from (irb):23:in `rescue in irb_binding'
ActiveRecord::RecordNotFound (Couldn't find Comment with 'id'=8)
```

記事を削除したことで関連付いていたコメントも削除できた。



<br>
##13. BASIC認証の実装
http_basic_authenticate_withメソッドを使って、ArticlesControllerに承認した人だけがアクセスできるようにする。

今回は、indexアクションとshowアクションは自由にアクセスできるようにし、それ以外のアクションには認証を要求する仕様にする。

app/controllers/articles_controller.rb

indexアクションの上に以下を追記。

```rb:articles_controller.rb
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
```

・`except: [:index, :show]`
exceptで認証を除外するアクションを指定。

http://localhost:3000/articles は表示できるが、そこからeditページに飛ぶとログイン承認画面が表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563526/d25cb543-8fa8-1a54-eb1f-a41cceaaab14.png)


###コメント削除にもBASIC認証を追加
CommentsControllerに、destroyのみBASIC認証をかける処理を追加する。

app/controllers/comments_controller.rb

createアクションの上に以下を追記。

```rb:comments_controller.rb
http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
```

・`only: [:destroy]`
onlyオプションで認証を除外するアクションを指定。


<br>
以上。