hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

映画レビューサイトのコード実装

トップページ

トップページでは、スクレイピングで追加した作品を、追加された順に20件表示する。

コントローラで最新順の作品データを取得

最新順にデータを取得するにはどうしたらいいのか?
ActiveRecordには順番を指定するorderメソッド、取得する件数を指定するlimitメソッドがある。

 orderメソッド

orderメソッドは、データベースからデータ取得時にデータをどう並び替えるかというのが指定できる。
例えば、「id順に並べたいとき」や「作成日順に並べたい」ときに使う。

引数には並び替える基準のカラムと、DESC(降順)ASC(昇順)のどちらかの2つを指定することにより順番を指定して取得することができる。

1
  モデル名.order('カラム名 順序')

実行例

実行例 詳細
User.order('id DESC') idを降順でユーザデータ取得
User.order('id ASC') idを昇順でユーザデータ取得

 limitメソッド

limitメソッド はデータをいくつ取得するかというのが指定できるメソッドで、
引数には取得したい件数を指定する。

1
  モデル名.limit(取得件数)

実行例

ユーザを10件取得する

1
User.limit(10)

 問題1:app/controllers/products_controller.rbのindexアクションに、productsテーブルから最新順に作品データを20件取り出す処理を書く

作業ファイル:app/controllers/products_controller.rb 

トップページに作品が表示されるようになったら成功。

 

要点チェック

 

トップページの作品一覧でタイトルを表示する

画像は作品それぞれの画像になっているが、作品タイトルがすべて「タイトル」になっている。このままでは作品タイトルがわからないので現在、「タイトル」となっているところをそれぞれの作品名にしたい。

 編集するファイル

この作品のビューはRailsアプリケーションのディレクトリのどこにあるのか。
コントローラはproducts_controllerなので「mooovi」>「app」>「views」>「products」のフォルダの中にビューのファイルが入っている。

    •  mooovi
      •  apps
        •  views
          •  products
            •  index.html.erb
            •  search.html.erb
            •  show.html.erb
          •  reviews
            •  new.html.erb

       

 

productsフォルダの中にはビューのファイルが2つ入っている。この中でトップの作品一覧を表示しているビューはindex.html.erb

このファイルを編集して「タイトル」の部分にそれぞれの作品の作品名を表示させるようにする。

 問題2:app/views/products/index.html.erbファイルを編集して作品タイトルを表示させる

作業ファイル:app/views/products/index.html.erb 

 

作品ごとに作品タイトルが表示されるようになったら成功。

 

作品ページ

トップページの作品をクリックしたら、作品ページへリンクする。

作品の情報を取得

まずは、コントローラから編集する。

作品ページを扱うアクションで作品のidを受け取り、そのidにあたる作品情報をデータベースから1件取得する。
idに当たる作品情報を1件取得するメソッドが、findメソッド

 findメソッド

findメソッドは引数に指定したidにあたる作品情報を1件だけ取得する。
もし、そのidにあたる作品が存在しない場合、エラーが発生する。

 

idが5のユーザを取得する

1
2
3
4
User.find(5)
# idが5のUserモデルのインスタンスが取得
# or
# idが5のUserモデルのインスタンスがない場合エラー

 

要点チェック

 

作品ページで呼び出されるアクションを探す

トップページの作品をクリックしたあと、作品ページに遷移する。その際、どんなHTTPリクエストが呼ばれているのか。完成版のmoooviでトップページの作品をクリックしてそのURLを見てみる。

 

上の画像では作品ページのURLはmooovi.tech-camp.in/products/20となっている。mooovi.tech-camp.inの部分はサイトのホスト名なので大事なのはパスの「/products/20」。このページを表示するアクションを調べるためにルーティングを見て行く。

 rake routesコマンド

rake routesコマンドはそのアプリケーションでアクセスできるHTTPリクエストの一覧を見ることのできるコマンド。このコマンドを使うとHTTPリクエストのメソッド(GET, POSTなど)とパス、それに対応するコントローラとアクション名が確認できる。

 moooviのディレクトリでrake routesコマンドを使ってみる

ターミナル
1
2
3
4
  $ cd ~/projects/mooovi
  # 「mooovi」ディレクトリに移動

  $ bundle exec rake routes

以下のようにターミナルに表示される。

ターミナル
1
2
3
4
            Prefix Verb URI Pattern                                 Controller#Action
   search_products GET  /products/search(.:format)                  products#search
           product GET  /products/:id(.:format)                     products#show
              root GET  /                                           products#index

いろいろと情報が表示されているが今のところは5行目の以下の文だけ見てみる。

ターミナル
5
product GET  /products/:id(.:format)                     products#show

GETとはHTTPリクエストの種類のこと。
そのあとに続いて/products/:id(.:format)と表示されている。これは先ほどの作品ページのURLにあった「/products/20」と対応している。
/products/:id(.:format)にある:idとは任意の作品idということ。上の例ではこの作品idは20。

ターミナル
1
2
/products/:id(.:format)
# :idは作品ページで表示される作品のidが該当する(例: 20)

ターミナルの一番右にはproducts#showと表示されている。これはproductsコントローラshowというアクションが呼ばれるという意味。/products/:id(.:format)のURLにGETでHTTPリクエストが送られると呼ばれる。

ターミナル
1
2
products#show
# productsコントローラのshowアクションが呼ばれる

つまり、rake routesコマンドで表示された文は以下のような意味を持っている。

URL

ではこのHTTPリクエストによって送られたidの取得方法を見ていく。

 params[:id]

HTTPリクエストが送られると、パス(/products/20)から作品idを取得することができる。
作品idはパラメーターとして自動的にparamsというハッシュ型の変数に格納されコントローラに送られる。またこの時キーは/products/:ididになり、バリューはアクセスしたパスの:idの箇所に一致する数字になる。

/products/20にアクセスした場合paramsという変数は以下のようになる。

1
2
puts params
# { id: 20 }

ハッシュから指定したキーのバリューを取得したい際は、以下のようにする。

1
2
puts params[:id]
# 20

 

要点チェック

 

作品ページで呼び出されるアクションを編集する

showアクションに作品ページで表示する作品情報を取得する処理を追加する。

 問題3:products_controller.rbのshowアクションに、productsテーブルから該当するidの作品情報を取得し`@product`の変数へ代入する処理を書く

作業ファイル:app/controllers/products_controller.rb
ヒント:パス/products/:id内の:idにあたる数字(作品のid)はコントローラ内でparams[:id]とすると取得できる 

 

検索機能

いよいよレビューを投稿する機能を作る。この映画レビューサイトではまず、投稿したい作品を検索して選び、作品名で検索できる検索機能を実装する。

 

コントローラで検索結果の作品データを取得

入力したキーワードを含んでいる作品を検索する。

作品タイトルにキーワードが含まれている作品を取得するにはLIKE句というものを使う。

 LIKE句

LIKE句は、曖昧(あいまい)な文字列の検索をすることができるもので、whereメソッドと一緒に使う。

曖昧な文字列の検索とはどういうことか。
例えば、1文字目に'a'という文字列が入ったデータや最後の文字に'b'が入っているデータ、文字列の途中に'c'が入ったデータなどを検索したい時に、曖昧文字列というものを使って検索すること。

曖昧文字列について

文字列 意味
% 任意の文字列(空白文字列含む)
_ 任意の1文字

実行サンプル

実行例 詳細
where('title LIKE(?)', "a%") aから始まるタイトル
where('title LIKE(?)', "%b") bで終わるタイトル
where('title LIKE(?)', "%c%") cが含まれるタイトル
where('title LIKE(?)', "d_") dで始まる2文字のタイトル
where('title LIKE(?)', "_e") eで終わる2文字のタイトル

 

要点チェック

 

検索したときに呼ばれるメソッド

moooviの右上の投稿するボタンから投稿画面に移動して、実際に作品を検索してみる。
すると、検索キーワードが消えますが作品は表示されない。まだ検索機能を実装していないので当然である。検索したあとのURLを見てみる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//87d6ef222b067e92856ec2dc3af35e50.png

URLが/products/search?utf8=✓&keyword=美女になっている。rake routesコマンドでこのURLがどのコントローラのアクションに対応しているか確認してみる。

ターミナル
1
  $ bundle exec rake routes
ターミナル
1
2
3
4
            Prefix Verb URI Pattern                                 Controller#Action
   search_products GET  /products/search(.:format)                  products#search
           product GET  /products/:id(.:format)                     products#show
              root GET  /                                           products#index

2行目の以下の部分の表示が/products/search?utf8=✓&keyword=に対応している文。

ターミナル
2
                search_products GET  /products/search(.:format)                 products#search

productsコントローラのsearchアクションが呼ばれていることがわかる。

 

productsコントローラのsearchアクションを編集する

productsコントローラのsearchアクションに入力された検索キーワードで曖昧検索をして、その結果を返す処理を追加する。

 問題4:products_controller.rbのsearchアクションに、productsテーブルのタイトルからキーワードで検索した作品データを`20件`取り出し、`@products`変数に入れる処理を書く

作業ファイル:app/controllers/products_controller.rb
ヒント:フォームに入力した文字はコントローラ内でparams[:keyword]で取得する。これは、検索バーを表示するinput要素のname属性の値
ヒント:取得するデータ数を制限するにはlimitメソッドを使う 

これでレビューを投稿する準備が整った。

投稿機能

続いて、レビュー投稿画面をつくるためにReviewモデルとapp/views/reviews/new.html.erbに入力フォームを作成していく。

 

現段階では投稿画面に遷移してもReviewモデルがないためにエラー画面が表示されてしまう。

レビューのモデルReviewを生成

レビューのモデルを生成する。モデル名はReviewにする。必要なカラムは何か?レビューの投稿画面から必要なカラムを考える。入力する箇所はニックネーム評価(10段階評価)レビューの内容である。この他に必要な情報はどの映画のレビューかという情報。これらをカラムとして保存する。

カラム名 データ型 用途
nickname string ニックネーム
rate integer 評価
review text レビュー
product_id integer 作品id

 

どの映画のレビューか、という情報はProductモデルのidがあれば十分である。よって作品idとしてproduct_idをカラムにする。

このカラムを持ったモデルReviewを作成する。

 問題5:Reviewモデルを作成し、必要なカラムを持ったテーブルを作成する。

ヒント:bundle exec rake db:migrateコマンドでマイグレーションファイルの実行

問題ができたら、実際にreviewsテーブルが作成されているか確認してみる。

https://tech-master.s3.amazonaws.com/uploads/curriculums//70ea041c381f656411481de68227f021.png

 

投稿フォームを作る

 レビューを投稿するためのフォームを作っていく。フォームに入力したデータは検索フォームと異なりReviewモデルのインスタンスとしてreviewsテーブルに保存する。その時はform_forヘルパーメソッドを使う。
 また、ルーティングを簡単に作成するためにresourcesメソッドを使って定義する。さらに今回は、どのproductsに対するreviewなのかをパスから簡単に判別できるようにresourcesメソッドをネストさせる。

今、投稿画面にアクセスすると「Routing Error」が表示される。これはまだ投稿画面のルーティングを設定していないためである。これからルーティングを設定していく。

config/routes.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
TechReviewSite::Application.routes.draw do
  get 'products/:product_id/reviews/new' => 'reviews#new'
  post 'products/:product_id/reviews' => 'reviews#create'

  resources :products, only: :show do
    collection do
      get 'search'
    end
  end
  root 'products#index'

end

これで投稿画面のルーティングを指定することができた。
ただこのように パス と アクション を自分で考えて定義していくのは冗長であるので、resourcesというメソッドを使ってルーティングを生成していく。

 resourcesメソッド

resourcesメソッドに対象となるリソース名(コントローラ名)を指定するだけで、Railsのリソースの7つのアクションで記載した7つのアクション名に対してのルーティングが自動的に設定される。

config/routes.rb
1
2
 Rails.application.routes.draw do
  resources :books

ターミナルにて bundle exec rake routes コマンドを実行すると以下のように7つのルーティングが設定されているのが確認できる。

1
2
3
4
5
6
7
8
9
 Prefix Verb   URI Pattern               Controller#Action
    books GET    /books(.:format)          books#index
          POST   /books(.:format)          books#create
 new_book GET    /books/new(.:format)      books#new
edit_book GET    /books/:id/edit(.:format) books#edit
     book GET    /books/:id(.:format)      books#show
          PATCH  /books/:id(.:format)      books#update
          PUT    /books/:id(.:format)      books#update
          DELETE /books/:id(.:format)      books#destroy

またcontrollerでbefore_actionの編集をした時と同様に、resourcesメソッドでもexceptやonlyオプションを組み合わせて特定のアクションを指定することができる。

config/routes.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
TechReviewSite::Application.routes.draw do
  resources :reviews, only: [:new, :create]

  resources :products, only: :show do
    collection do
      get 'search'
    end
  end
  root 'products#index'

end
ターミナル
1
$ rake routes
ターミナル
1
2
3
4
5
6
         Prefix Verb URI Pattern                Controller#Action
        reviews POST /reviews(.:format)         reviews#create
     new_review GET  /reviews/new(.:format)     reviews#new
search_products GET  /products/search(.:format) products#search
        product GET  /products/:id(.:format)    products#show
           root GET  /                          products#index

このようにresourcesメソッドを使えばRailsの規則に適ったルーティングを定義することができる。

3行目の /reviews/new はreviewの新規作成のページへのパスになる。
しかしこのままではどのproductに対するreviewなのかということを表す情報がない。これはresourcesメソッドをネストさせる(入れ子構造にする)ことによって取得することができる。

 resourcesメソッドのネスト

resourcesメソッドのネストは以下のように書く。

config/routes.rb
1
2
3
resources :books do
  resources :reviews
end

bundle exec rake routesコマンドを実行すると以下のように表示される。

ターミナル
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
          Prefix Verb   URI Pattern                                Controller#Action
    book_reviews GET    /books/:book_id/reviews(.:format)          reviews#index
                 POST   /books/:book_id/reviews(.:format)          reviews#create
 new_book_review GET    /books/:book_id/reviews/new(.:format)      reviews#new
edit_book_review GET    /books/:book_id/reviews/:id/edit(.:format) reviews#edit
     book_review GET    /books/:book_id/reviews/:id(.:format)      reviews#show
                 PATCH  /books/:book_id/reviews/:id(.:format)      reviews#update
                 PUT    /books/:book_id/reviews/:id(.:format)      reviews#update
                 DELETE /books/:book_id/reviews/:id(.:format)      reviews#destroy
           books GET    /books(.:format)                           books#index
                 POST   /books(.:format)                           books#create
        new_book GET    /books/new(.:format)                       books#new
       edit_book GET    /books/:id/edit(.:format)                  books#edit
            book GET    /books/:id(.:format)                       books#show
                 PATCH  /books/:id(.:format)                       books#update
                 PUT    /books/:id(.:format)                       books#update
                 DELETE /books/:id(.:format)                       books#destroy

では、ルーティングを定義する。

config/routes.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
TechReviewSite::Application.routes.draw do
  resources :products, only: :show do
    resources :reviews, only: [:new, :create]
    collection do
      get 'search'
    end
  end
  root 'products#index'

end

4行目にあるcollection というのは、Rails7つのアクション以外のアクション名を定義する時に使う。

 bundle exec rake routesコマンドでルーティングを確認する

ターミナル
1
2
3
4
5
6
            Prefix Verb URI Pattern                                 Controller#Action
   product_reviews POST /products/:product_id/reviews(.:format)     reviews#create
new_product_review GET  /products/:product_id/reviews/new(.:format) reviews#new
   search_products GET  /products/search(.:format)                  products#search
           product GET  /products/:id(.:format)                     products#show
              root GET  /                                           products#index

これでどのproductに対するreviewなのかということがパスから判断できるようになった。

最後にビューのフォームを編集していく。
今回入力フォームから送られたデータはReviewモデルのインスタンスとしてreviewsテーブルに保存する。その時はform_forというヘルパーメソッドを使う。

 form_for

form_forは、特定のモデルを編集・追加するためのフォームを生成するヘルパーメソッド。
特定のテーブルにレコードだけを新規作成、更新するときに利用する。

1
2
3
<%= form_for(モデルクラスのインスタンス) do |f| %><% end %>

このようにあるモデルのインスタンスをform_forの引数にする。form_tagでは送信先のurlを直接引数に定義した。

search.html.erb
1
<%= form_tag('/products/search', method: :get) do %>

form_forは引数のインスタンスが何も情報を持っていなければ自動的にcreateアクションへ、すでに情報を持っている場合はupdateアクションへ自動的に振り分けてくれる。

次にform_for内でフォームを作るメソッドについて。

form_for内で使うメソッドは、f.htmlタグ名 :カラム名の形で指定する。

1
<%= f.text_field :name %>

こちらは、次のようなhtmlに変換される。

1
<input id="モデル名_name" name="モデル名[name]" type="text" size="モデルで設定したsize">

form_for内におけるメソッドはformに使用するhtmlタグの数だけある。

メソッド 用途
f.label labelのlabelタグを表示 
f.text_field textのinputタグを表示
f.date_select モデルで設定したフィールドをselectタグで選べるようにして表示
f.check_box checkboxのinputタグを表示
f.number_field numberのinputタグを表示
f.submit submitのinputタグを表示

基本的にhtmlのformを知っていたら推測できるようなメソッドが用意されている。必要なときに都度調べる。

 form_forとform_tag

form_for と form_tag どちらを使うべきかは、基本的にモデルの有無で判断する。入力フォームで入力するデータのモデルがあれば form_for を使い、入力するデータが特にモデルを持っていなければ form_tag を使う。

  • form_for: モデルがあるデータを扱うときに使う。(投稿フォームなど)
  • form_tag: 単にデータを特定のアクションに送りたい時に使う。(検索フォームなど)

form_for

今回は、レビュー投稿画面views/reviews/new.html.erbに常に新規作成されたReviewクラスのインスタンスを渡す。
フォームに入力された値は、投稿ボタンを押した瞬間にReviewクラスの新規インスタンスにそれぞれの属性の値としてセットされ、インスタンスはそのままreviewsテーブルに保存される。
このような関係を簡単に実現してくれるのがform_for である。

【例】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<div class="row form">
  <div class="col-lg-12">
    <%= form_for current_user do |f| %>
      <h2>登録情報</h2>
      <div class="form-group">
        <div class="col-lg-6">
          <%= f.text_field :full_name, placeholder: "フルネーム", class: "form-control" %>
        </div>
      </div>
      <div class="form-group">
        <%= f.submit "送信", class: "btn btn-primary withripple" %>
      </div>
    <% end %>
  </div>
</div>

eachメソッドのように3行目で|変数| で変数を定義しており、さらに7行目と11行目でf. ~ としている部分で、属性値をセットしている様子が直感的にわかる。このように、「モデルの新規インスタンスに値を追加して保存」したい場合はform_for を利用する。

form_tag

一方で「となりのトトロ」と入力すると、モデルを介さずにコントローラにてproductsテーブルから該当する情報を引用し、結果をビューに表示した検索機能ではfom_tagを利用した。

 

new.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
 <div id="main_cnt_wrapper">
  <div id="yjContentsBody">
    <div class="yjContainer">
      <span class="yjGuid"><a id="yjContentsStart" name="yjContentsStart"></a><img alt="ここから本文です" height="1" src="http://i.yimg.jp/yui/jp/tmpl/1.1.0/audionav.gif" width="1"></span>
      <div id="yjMain">
        <article class="section">
          <div class="container">
            <header class="header header--section">
              <h2 class="text-middle">
                <i class="icon-movie color-gray-light"></i>投稿
              </h2>
            </header>
            <div>
              <ul class="listview js-lazy-load-images">
                <li style="margin-bottom: 15px">
                  <a class="listview__element--right-icon" href="#">
                    <div class="box">
                      <div class="box__cell w80">
                        <div class="thumbnail thumbnail--photo">
                          <div class="thumbnail__figure" style="background-image: url(<%= @product.image_url %>);"></div>
                        </div>
                      </div>
                      <div class="box__cell pl1em">
                        <h3 class="text-middle text-break color-sub">
                          <%= @product.title %>
                        </h3>
                        <p class="text-xsmall">
                        </p>
                        <p class="text-xsmall opacity-60"></p>
                      </div>
                    </div>
                  </a>
                </li>
              </ul>
            </div>
          </div>
          <%= form_for [@product, @review] do |f| %>
          <div style="margin: 8px 0">
            <%= f.label :nickname, style: { 'margin-right' => 8 } %>
            <%= f.text_field :nickname, placeholder: 'nickname', value: '' %>
          </div>
          <div style="margin: 8px 0">
            <%= f.label :評価, style: { 'margin-right' => 8 } %>
            <%= f.number_field :rate, placeholder: '評価', value: 1, max: 10, min: 1, html: { class: "search__query", style: 'text-align: right;' } %>
          </div>
          <div style="margin: 8px 0">
            <%= f.text_area :review, placeholder: 'レビューを書いてね!', style: 'width: 100%;height: 300px;' %>
          </div>
          <div class="row">
            <div class="col10 push1">
              <%= button_tag type: "submit", class: "btn btn--block" do %>
              投稿する<i class="icon-arrow-right"></i>
              <% end %>
            </div>
          </div>
          <% end %>
        </article>
      </div>
      <div id="yjSub">

37行目のform_forの引数が配列になっている。配列の1つ目の要素のインスタンスのidがパスの:product_idに入る。今回はパスのネストが2段階なので、form_forの引数のインスタンスは2つ。つまり、resourcesメソッドを2回ネストさせた場合、form_forの引数のインスタンスは3つになる。

resourcesメソッドを2回ネストした場合

config/routes.rb
1
2
3
4
5
resources :users do
  resources :products do
    resources :reviews
  end
end
reviews_controller.rb
1
2
3
4
5
def new
  @user = User.find(4)
  @product = Product.find(2)
  @review = Review.new
end
new.html.erb
1
2
3
<%= form_for ( [@user, @product, @review ] ) do |f| %>
  (中略)
<% end %>
送られるリクエス
1
2
3
4
# HTTPメソッド
POST
# パス
/users/4/product/2/reviews

これで投稿画面が完成。

試しにレビューを投稿してみる

試しに、いまの段階でフォームに値を入力して投稿ボタンを押しても何も変化がない。

RailsではHTTPリクエストを送ったあと、レスポンスとして ビュー を返す。
レビューの投稿では reviewsコントローラのcreateアクション が呼び出される。本来その後に、「views」->「reviews」->「create.html.erb」が表示される。

しかし、ディレクトリを見るとわかるようにreviewsの中にcreate.html.erbはない。つまり、createアクションに対応したビューが存在しないということ。表示させるコンテンツがないため画面の遷移が起きなかった。

投稿が完了したらトップページに移動する

そこで、投稿されたらトップページへ遷移するように実装する。
このようにアクセスしたページとは違うページへ移動することをリダイレクトと言う。リダイレクトをする場合はアクションのビューが呼ばれない。そのため、ビューが無くてもエラーがでない。

 リダイレクト

本来アクセスしたページとは別のページへ移動することを言う。

railsではredirect_toというメソッドでリダイレクトさせることができ、actionオプションを使用することで特定のアクションを指定することができる。

1
redirect_to action: アクション名

という書き方ができる。

 問題6:createアクションですべての処理が終わった後、トップページにリダイレクトする処理を書く

ヒント:redirect_toメソッドでコントローラとアクションを指定する 

要点チェック

 

コントローラで投稿データをデータベースに保存

reviewsコントローラのcreateアクションでフォームから送られてきた値を使ってReviewクラスのインスタンスを作成する処理を書く。これでレビューが投稿できるようになる。form_forから送られてきたパラメーターはrequireメソッドを使ってデータを取得する。

 requireメソッド

form_forに入力されたデータはparamsの中に以下のような形でコントローラに送られる。

【例】
new.html.erb
1
2
3
4
<%= form_for(@book) do |f| %>
  <%= f.text_field :name %>
  <%= f.text_area :detail %>
<% end %>
ターミナル
1
2
params
# { book: { name: "入力された名前", detail: "入力された詳細" } }

キーはform_forの引数にあるインスタンスの モデル名(book) になり、バリューはカラム名と入力された値のハッシュ。
paramsからキーがbookのバリューを取得し、createアクションの引数に渡せるようにストロングパラメーターを定義するには以下のようにする。

【例】
books_controller.rb
1
2
3
4
params
# { book: { name: "入力された名前", detail: "入力された詳細" } }
params.require(:book)
# => { name: "入力された名前", detail: "入力された詳細" }

 form_tagとform_forでの保存方法の違いについて

Pictweetではform_tagを使用して入力情報を保存したが、Moooviではform_forを使用して入力情報を保存した。以下のように、Pictweetではコントローラで入力情報を保存する際、ストロングパラメーターとしてparams.permit(:キー名)という記述をしたが、Moooviではparams.require(:モデル名).permit(:カラム名)と記述した。

pictweet/app/controllers/tweets_controller.rb
1
2
3
4
5
6
7
8
class TweetsController < ApplicationController
# 省略

  def tweet_params
    params.permit(:image, :text)
  end

# 省略
mooovi/app/controllers/reviews_controller.rb
1
2
3
4
5
6
7
8
class ReviewsController < RankingController
# 省略

  def create_params
    params.require(:review).permit(:rate, :review) #  省略
  end

# 省略

この記述方法の違いは、paramsの構造の違いによるものである。
form_tagを使用したPictweetの場合は以下の様な構造をしている(確認したい場合は、binding.pryを使ってみる)。

Pictweetのparamsの構造
1
2
3
4
5
6
7
8
=> {
       "utf8"=>"✓",
       "authenticity_token"=>"/3QchQigdEcpc2VaOn+6IGIV14x1iJ5bDsM7GI5NA6k=",
       "image"=>"http://sample/hoge.jpg",
       "text"=>"hello!",
       "controller"=>"tweets",
       "action"=>"create"
   }

これに対して、form_forを使用したMoooviの場合は以下の様な構造をしている。

Moooviのparamsの構造
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
=> {
       "utf8"=>"✓",
       "authenticity_token"=>"/3QchQigdEcpc2VaOn+6IGIV14x1iJ5bDsM7GI5NA6k=",
       "review"=> {
          "rate"=>"5", 
          "review"=>"It's reasonable"
        },
       "action"=>"create",
       "controller"=>"reviews",
       "product_id"=>"4"
   }

ポイントは、paramsの中で、自身が入力した情報を含む箇所のハッシュ構造。

form_tagを使用したPictweetのparamsは、ビューファイルのフォームで与えたname属性キーとなっている。そして、それぞれのキーに対して、入力した情報がとして紐付いている。このハッシュ構造を生成するform_tagでは、params.permit(:キー名)で入力した情報が取得できる。

これに対してform_forを使用したMoooviのparamsは、ハッシュの中にハッシュが存在する二重構造になっている。reviewというキーに対応するそのものがハッシュだからで、そのハッシュの中には、カラム名キーとなり、入力した情報がとして紐付いている。このハッシュ構造を生成するform_forでは、params.require(:モデル名).permit(:カラム名)で入力した情報が取得できる。

ハッシュの二重構造.png

このように、paramsのハッシュ構造の違いによって、入力した情報の保存方法も変わってくる。これは非常に重要な点。

2つのハッシュを結合してみる

ここではそれ以外にパスで送られてきたproduct_idも取得しなければならない。

product_idもparamsとして送られてきている。paramsはハッシュになっているので、2つのハッシュを結合できればうまく取得できそうである。

 mergeメソッド

mergeメソッドは2つのハッシュを統合する時に使うメソッド。

1
2
3
4
hash1 = {question: "Who is your favorite artist?"}
hash2 = {group: "tech_camp", name: "abe"}
hash1.merge(hash2)
#=> {:question=>"Who is your favorite artist?", :group=>"tech_camp", :name=>"abe"}

投稿フォームで送られてきたparamsに、パスで送られてきたidなどのparamsを追加するときにはこの方法を使う。

 問題7:reviews_controller.rbのcreateアクションでレビューデータを受け取り、データベースのReviewsテーブルに保存する処理を書く

ヒント:Reviewクラスにcreateメソッドを実行する

ここまでできたら、実際に投稿してデータが保存されているか確認。

まずはレビューを投稿。

 

次に、reviewsテーブルに投稿した情報が保存されているか確認。

https://tech-master.s3.amazonaws.com/uploads/curriculums//30f9527cdf9eb11c76993f17c370df01.png

 

作品ページから投稿するボタンを作る

ヘッダーの右の「投稿する」ボタンからはレビューを投稿できるようになったが、いちいち作品を検索して選ぶのは大変。せっかくトップ画面に作品の一覧が並んでいるのでここで作品を選んでレビューを投稿したいので作品ページからレビューを投稿できるようにする。

作品ページに移動すると、

作品ページの右下に「この作品を投稿する」というリンクがある。いまはクリックしても何も起きない。ビューを編集してこのリンクを押すとその作品のレビュー投稿画面に遷移する実装する。

 

作品ページのビューはproductsコントローラのビューのディレクトリに入っている。

    •  mooovi
      •  apps
        •  views
          •  products
            •  show.html.erb

       

 

作品ページではproductsコントローラのshowアクションが呼ばれるのでビューはそれに対応するshow.html.erb。このファイルを編集してレビュー投稿画面に遷移するようにする。

 問題8:作品ページの「この作品を投稿する」というリンクをクリックするとその作品のレビュー投稿画面に遷移するように実装する

作業ファイル:app/views/products/show.html.erb
ヒント:productsコントローラのshowアクションで定義した@productを使う
ヒント:newアクションに対応するパスはrake routesコマンドで確認する

 

書いたレビューを表示

レビューは書けるようになったが、肝心のレビューがどこにも表示されていない。作品ページに移動してみると、

「みんなのレビュー」というところにレビューが表示されている。いまはサンプルのレビューが書かれている。ここを本当のレビューを表示させるようにする。

アソシエーションを設定

レビューのモデルReviewと作品のモデルProductの間にアソシエーションを設定する。ReviewモデルとProductモデルの間には以下のような関係がある。

① 1つの作品(Productモデル)は複数のレビューを持っている

様々なユーザーが作品についてのレビューを書くため、1つの作品は複数のレビューを持っている。

② 1つのレビュー(Reviewモデル)は1つの作品に属している

レビューはある特定の1つの作品についてのものなので、1つのレビューはただ1つの作品と紐づいている。

 

1つのProductモデルは複数の Reviewモデルをもつ

1つのReviewモデルは1つのProductモデルを持つ

 

以上より、ProductモデルとReviewモデルの間には1対多の関係がある。

この関係をProductモデルとReviewモデルに記述する。アソシエーションの記述はbelongs_tohas_manyを宣言。

 問題9:1対多のアソシエーションをProductモデルとReviewモデルで宣言する

作業ファイル:app/models/product.rb, app/models/review.rb
ヒント:has_many,belongs_toをそれぞれ指定する 

アソシエーションを宣言できたので、rails cで確認。

ターミナル
1
$ rails c

 以下の流れをrails cで試してProductモデルとReviewモデルのアソシエーションを確認。

  • ReviewモデルからProductモデルを取得できるか確認
ターミナル
1
2
3
  review = Review.first # 一番最初のレビューを取得
  review.product
  => その作品情報が表示されるか
  • ProductモデルからReviewモデルを取得できるか確認
ターミナル
1
2
3
4
  review = Review.first # 一番最初のレビューを取得
  product = review.product # productを変数に入れる
  product.reviews
  => その作品のレビューの一覧が表示されるか

 

作品ページにレビューの一覧を表示

作品ページでレビューの一覧を表示する。今のままではサンプルのレビューが表示された状態。作品ページのHTMLが書いてあるのはapp/views/products/show.html.erbなのでshow.html.erbを編集する。

erbファイルはHTML内でerb記法を用いることによりRubyを書くことができるのでアソシエーションを使うことができる。show.html.erb内でアソシエーションを使って作品のレビューの一覧を取得し、それらを表示する。

 問題10:作品ページでレビューの一覧が表示されるように show.html.erbを編集する

作業ファイル:app/views/products/show.html.erb
ヒント:1つのレビューを表す<li></li>をeachメソッドを使用して@product.reviewsの数だけ繰り返し表示する

編集できたら、作品のレビューが表示されるか確認する。

 

これで基本的な映画レビューアプリケーションの機能は完成。