【Rails】ネストしたresourcesをフル活用したアプリケーションの作成方法|_url, _path, form_with modelのエラー対処法

rails-prograshi(プロぐらし)-kv Rails
記事内に広告が含まれていることがあります。

DB操作を伴うアプリケーションを作成するときに、ルーティングでresourcesを使うと、DB操作に必要な8つのルーティングをまとめて生成してくれます。

resourcesで生成されるルート一覧

アクションHTTP動詞パス内容
indexGET/コントローラ名一覧を表示
createPOST/コントローラ名データ追加処理
newGET /コントローラ名/newデータ追加用の画面
editGET/コントローラ名/:id/edit指定したデータを変更する画面
showGET/コントローラ名/:id指定したデータの詳細画面
updatePATCH/コントローラ名/:id指定したデータの更新処理(変更箇所のみ)
updatePUT/コントローラ名/:id指定したデータの更新処理(全体を更新)
destroyDELETE/コントローラ名/:id指定したデータの削除処理

(参考)【Rails】ルーティングのresourcesとは?意味と使い方

このresourcesはネストして使うこともできます

resourcesをネストさせると、例えば、1つの製品(product)に対して複数のレビュー(review)を持たせるといった操作ができるようになります。

Youtubeやtwitterの1つの投稿に対して、複数のレビューが紐づくイメージです。

このようなアプリケーションを作る解説はWEB上でたくさん見つけることができますが、レビューで実行できるアクションが限られている場合があります。

ここでは、2つのresources(上記の例であれば、製品とレビュー)それぞれで、投稿・編集・一覧・詳細・削除ができるアプリケーションを作成する方法を実例で解説しています。

なお、ファイルの生成はDB操作時に便利なscaffoldを使っています。ですが、デフォルトで生成されたファイルをそのまま使おうとするとエラーが大量発生します。これらのエラーの解消方法をまとめて解説しています。


ネストしたresourcesで作成するアプリケーションの概要

製品のデータを持つproductsテーブルと、各商品毎の複数のレビュー持つreviewsテーブルを作成します。

productsテーブルと、revirewsテーブルは1対多の関係(1つの製品に複数のレビューがつけられる関係)にします。

テーブルの関係性に合わせて、ルーティングもproductsの配下にreviewsがくるようにします。

ある商品のレビュー一覧を見るときは  /products/:products_id/reviews
その中の一つのレビューの詳細ページを見るとき /products/:products_id/reviews/:id

のようにします。

モデル、マイグレーションファイル、コントローラ、ビューなど必要なファイルはRailsの超便利なscaffoldコマンドを使って生成します。

scaffoldの使い方や生成されるファイルの詳細については下記をご参考ください。

【Rails】超便利コマンドscaffoldの使い方|何が起こっているかを完全理解


必要なファイルの作成

早速、scaffoldで必要なファイルを作成します。scaffoldの基本構文は次のようになります。

rails g scaffold モデル名 <カラム名1:型1> <カラム名2:型2>,,,

製品(product)関連のファイルを作成

prodcut関連のファイルを作成します。productsテーブルには、文字列型のnameカラムと、整数型のpriceカラムを作ります。

# rails g scaffold product name:string price:integer

▼実行例

# rails g scaffold product name:string price:integer
Running via Spring preloader in process 197
      invoke  active_record
      create    db/migrate/20210731102424_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml
      invoke  resource_route
       route    resources :products
      invoke  scaffold_controller
      create    app/controllers/products_controller.rb
      invoke    erb
      create      app/views/products
      create      app/views/products/index.html.erb
      create      app/views/products/edit.html.erb
      create      app/views/products/show.html.erb
      create      app/views/products/new.html.erb
      create      app/views/products/_form.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/products_controller_test.rb
      create      test/system/products_test.rb
      invoke    helper
      create      app/helpers/products_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/products/index.json.jbuilder
      create      app/views/products/show.json.jbuilder
      create      app/views/products/_product.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/products.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.scss

必要なファイルをまとめて生成することができました。とても便利です。


レビュー(review)関連のファイルを作成

review関連のファイルを作成します。productsテーブルに所属する関係になるので、カラムと型に、product:referencesを指定します。

併せて、整数型のrateカラムと、text型のreviewカラムを生成します。

referencesやテーブルに1対多の関係を持たせる方法の詳細については下記をご参考ください。

【Rails】references型とは何か?わかりやすく解説

# rails g scaffold review product:references rate:integer review:text

▼実行例

# rails g scaffold review product:references rate:integer review:text
Running via Spring preloader in process 172
      invoke  active_record
      create    db/migrate/20210731102330_create_reviews.rb
      create    app/models/review.rb
      invoke    test_unit
      create      test/models/review_test.rb
      create      test/fixtures/reviews.yml
      invoke  resource_route
       route    resources :reviews
      invoke  scaffold_controller
      create    app/controllers/reviews_controller.rb
      invoke    erb
      create      app/views/reviews
      create      app/views/reviews/index.html.erb
      create      app/views/reviews/edit.html.erb
      create      app/views/reviews/show.html.erb
      create      app/views/reviews/new.html.erb
      create      app/views/reviews/_form.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/reviews_controller_test.rb
      create      test/system/reviews_test.rb
      invoke    helper
      create      app/helpers/reviews_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/reviews/index.json.jbuilder
      create      app/views/reviews/show.json.jbuilder
      create      app/views/reviews/_review.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/reviews.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.scss


モデルファイルの編集

product.rbの編集

productsテーブルとreviewsテーブルが 1対多 の関係になるようにProductモデルのファイルを編集します。

class Product < ApplicationRecord
    has_many :reviews
end
注意点

has_manyで指定するのは、複数形の小文字です。

has_manyと対になる、belongs_toは 単数形の小文字 です。


review.rbの編集

reviewモデルはscaffoldの実行時にreferences型を指定したことで、belongs_to :productが自動で記載されているので、編集不要です。

もし、references型を指定していない場合は、以下のようにします。

class Review < ApplicationRecord
  belongs_to :product
end


マイグレーションの実行

rails db:migrateを実行し、マイグレーションファイルの内容をDBに反映させます。

# rails db:migrate
== 20210731102330 CreateProducts: migrating ===================================
-- create_table(:products)
   -> 0.0558s
== 20210731102330 CreateProducts: migrated (0.0560s) ==========================

== 20210731102424 CreateReviews: migrating ====================================
-- create_table(:reviews)
   -> 0.0808s
== 20210731102424 CreateReviews: migrated (0.0810s) ===========================

なお、productとreviewのマイグレーションファイルは次のようになっています。scaffoldの生成時にカラムを指定し忘れたり、referencesを指定し忘れたときは手動での編集が必要になります。


productsテーブル用のマイグレーションファイル

productsテーブル用のマイグレーションファイルは次のようになっています。

class CreateProducts < ActiveRecord::Migration[6.1]
  def change
    create_table :products do |t|
      t.string :name
      t.integer :price

      t.timestamps
    end
  end
end

reviewsテーブル用のマイグレーションファイル

reviewsテーブル用のマイグレーションファイルは次のようになっています。

class CreateReviews < ActiveRecord::Migration[6.1]
  def change
    create_table :reviews do |t|
      t.references :product, null: false, foreign_key: true
      t.integer :rate
      t.text :review

      t.timestamps
    end
  end
end

t.references :product, null: false, foreign_key: true が、scaffoldを実行するときにreferences型を指定したことで追記された部分です。

productテーブルと関連付けを行い、foreign_keyをtrueにしています。

tips

foreign_keyとは、指定したカラムの自由な記述を許可せず、指定したカラムの値のみしか使えないようにする制限です。

自分以外のテーブルのデータをキーとして参照するので、外部キーとも呼びます。


指定するには、マイグレーションファイルで、対象のカラムのオプションにforeign_key: trueを追記します。

例えば、次の場合は、productsテーブルを外部キーとして指定するという意味になります。こうすることで、このカラムはproductsテーブルにある値しか選択できないように制限がかかります。

t.references :product, null: false, foreign_key: true

なお、null: falseは未記入を許可しないという意味です。つまり、productsテーブルのデータの指定を必須としています。


ルーティングの作成と確認

ルーティングの作成

ルーティングファイルのネストしたresourcesを追加します。

config > routes.rb を以下のように編集します。scaffoldで自動生成されたルーティングは不要なので削除します。

Rails.application.routes.draw do
  resources :products
  resources :reviews
end

↓ 編集後

Rails.application.routes.draw do
  resources :products do
    resources :reviews
  end
end

これで、productsの配下にreviewsがつくルーティングにすることができました。

ルーティングの確認

ターミナルで、rails routesコマンドを実行すると、現在割り当てられているルーティングの一覧を確認することができます。

# rails routes
Prefix               Verb   URI Pattern                                  Controller#Action
product_reviews      GET    /products/:product_id/reviews(.:format)      reviews#index
                     POST   /products/:product_id/reviews(.:format)      reviews#create
new_product_review   GET    /products/:product_id/reviews/new(.:format)  reviews#new
edit_product_review  GET    /products/:product_id/reviews/:id/edit(.:format) reviews#edit
product_review       GET    /products/:product_id/reviews/:id(.:format)   reviews#show
                     PATCH  /products/:product_id/reviews/:id(.:format)   reviews#update
                     PUT    /products/:product_id/reviews/:id(.:format)   reviews#update
                     DELETE /products/:product_id/reviews/:id(.:format)   reviews#destroy
products             GET    /products(.:format)                           products#index
                     POST   /products(.:format)                           products#create
new_product          GET    /products/new(.:format)                       products#new
edit_product         GET    /products/:id/edit(.:format)                  products#edit
product              GET    /products/:id(.:format)                       products#show
                     PATCH  /products/:id(.:format)                       products#update
                     PUT    /products/:id(.:format)                       products#update
                     DELETE /products/:id(.:format)                       products#destroy

ネストしていない場合とは異なり、reivewsのルートがproductsの配下に配置されていることがわかります。

tips

reviewsのルートを指定するときは、パラメータで :product_id を渡す必要があります。

1つのproductに対するreview(s)であることを指定しています。

注意点

ネストするのはURI(パス)のみで、コントローラやビューファイルネストしません

ネストした先(内側)のresourcesに対応するコントローラファイルやビューファイルはネストする必要がありません(ネストしてはいけません)。


通常のrails g scaffoldrails g controllerでコントローラやビューを作成した状態のままで問題ありません。

controllers
 |- products
 |- reviews
views
 |- products
 |- reviews

通常のディレクトリの状態でルーティングのみネストできるのが、ネストさせたresourcesの便利な点です。


コントローラの編集

resourcesをネストさせたことで、ルーティングが通常の状態から大きく変化しています。これに合わせてコントローラも変更する必要があります。

reviewsコントローラの編集

reviewsのルートは、/products/:product_id/reviewsや、/products/:product_id/reviews/:idのように、product_idが必要になっています。

また、reviewsで表示するページにどの製品に属するページかを示すために、productのデータを渡すようにします。

class ReviewsController < ApplicationController
  before_action :set_review, only: %i[ show edit update destroy ]

  # GET /reviews or /reviews.json
  def index
    @reviews = Review.where(product_id:params[:product_id])
    @product = Product.find(params[:product_id])
  end

  # GET /reviews/1 or /reviews/1.json
  def show
  end

  # GET /reviews/new
  def new
    @review = Review.new
    @product = Product.find(params[:product_id])
  end

  # GET /reviews/1/edit
  def edit
  end

  # POST /reviews or /reviews.json
  def create
    @review = Review.new(review_params)

    respond_to do |format|
      if @review.save
        format.html { redirect_to product_reviews_url, notice: "レビューを作成しました。" }
        format.json { render :show, status: :created, location: @review }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @review.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /reviews/1 or /reviews/1.json
  def update
    respond_to do |format|
      if @review.update(review_params)
        format.html { redirect_to product_reviews_url, notice: "レビューを更新しました。" }
        format.json { render :show, status: :ok, location: @review }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @review.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /reviews/1 or /reviews/1.json
  def destroy
    @review.destroy
    respond_to do |format|
      format.html { redirect_to product_reviews_url, notice: "Review was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_review
      @review = Review.find(params[:id])
      @product = Product.find(params[:product_id])
    end

    # Only allow a list of trusted parameters through.
    def review_params
      params.require(:review).permit(:product_id, :rate, :review)
    end
end

reviewsコントローラの変更点は以下です。

  • newアクションに、@prodcutを追加
  • set_reviewsメソッドに、@productを追加(show, edit, update, destoryアクションに渡す)
  • redirect_toのリダイレクト先を、product_reviews_urlに変更(reviews_urlは存在しないのでエラーになる)

newアクションに@prodcutを追加

reviewsコントローラのnewアクションを実行したときに、レビューの一覧情報だけでなく、対象の製品情報も渡すようにします。

  def new
    @review = Review.new
    @product = Product.find(params[:product_id])
  end

Product.find(id番号)は引数にid番号を渡すと、そのデータをproductsテーブルから抽出してくる処理です。

params[:product_id]は、URIに含まれている :product_idにマッチするパラメータを取得します。パラメータの場所や名前はルーティングで定義されています。

例:/products/:product_id/reviews


set_reviewsメソッドに@productを追加

reviewsコントローラのshow, edit, update, destoryアクションを実行したときに、レビューの詳細情報だけでなく、対象の製品情報も渡すようにset_reviewメソッドに追記します。

なお、set_reviewメソッドは、冒頭のbefore_actionで実行されます。

    def set_review
      @review = Review.find(params[:id])
      @product = Product.find(params[:product_id])
    end


redirect_toのリダイレクト先をproduct_reviews_urlに変更

updateとdestroyアクション実行後に、redirect_toでリダイレクト先のパスが指定されています。デフォルトではreviews_urlが指定されています。

ですが、resourcesでネストさせたことでパスが変わっているので、それに合わせて修正します。

format.html { redirect_to product_reviews_url, notice: "レビューを更新しました。" }
format.html { redirect_to product_reviews_url, notice: "Review was successfully destroyed." }

format.htmlについては以下をご参考ください。


productsコントローラの編集

productsのルートはネストせずデフォルトの状態なので、productsコントローラを編集しなくても問題なく動きます。

ここでは、productsの詳細ページにアクセスしたときに、その製品のレビューが存在すれば、レビュー一覧を合わせて表示したいので、showアクションにreviewsのデータを渡すように編集します。

class ProductsController < ApplicationController
  before_action :set_product, only: %i[ show edit update destroy ]

  # GET /products or /products.json
  def index
    @products = Product.all
  end

  # GET /products/1 or /products/1.json
  def show
    @reviews = Review.where(product_id:params[:id])
  end

  # GET /products/new
  def new
    @product = Product.new
  end

  # GET /products/1/edit
  def edit
  end

  # POST /products or /products.json
  def create
    @product = Product.new(product_params)

    respond_to do |format|
      if @product.save
        format.html { redirect_to @product, notice: "Product was successfully created." }
        format.json { render :show, status: :created, location: @product }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /products/1 or /products/1.json
  def update
    respond_to do |format|
      if @product.update(product_params)
        format.html { redirect_to @product, notice: "Product was successfully updated." }
        format.json { render :show, status: :ok, location: @product }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /products/1 or /products/1.json
  def destroy
    @product.destroy
    respond_to do |format|
      format.html { redirect_to products_url, notice: "Product was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_product
      @product = Product.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def product_params
      params.require(:product).permit(:name, :price)
    end
end

変更箇所はshowアクションのみです。

  def show
    @reviews = Review.where(product_id:params[:id])
  end

各製品のreviewsのデータを渡すには、whereメソッドを使ってその製品のレビュー一覧を取得します。

モデルクラス.where(カラム名: 値)とすれば、指定したカラムの値が一致するデータを取得します。

whereメソッドの詳細については下記をご参考ください。

【Rails】whereメソッドの使い方まとめ。モデルを使って取得するテーブルの検索条件の絞り込みをする方法

注意点

prodcutsコントローラのshowアクションで開くビューに渡したreviewsの情報を一つづつ表示するために、eachメソッドを使います。

eachメソッドはActiveRecord::Relation型のデータに使うことができますが、オブジェクトでは使えません。

このため、whereメソッドの代わりに、findやfind_byメソッドで取得したデータを渡すとエラーになります。


Productsのビューファイルの編集

productsのページにアクセスしたときに、各製品ごとにレビューのリンクを表示したり、詳細ページでレビューの投稿をするために、views > prodcutsディレクトリ配下のビューファイルを編集します。

products > index.html.erb

製品一覧ページの各製品の横に、対応するレビュー一覧ページへのリンクを設置します。

<p id="notice"><%= notice %></p>

<h1>Products</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Price</th>
      <th colspan="4"></th>
    </tr>
  </thead>

  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.name %></td>
        <td><%= product.price %></td>
        <td><%= link_to 'Show', product %></td>
        <td><%= link_to 'Edit', edit_product_path(product) %></td>
        <td><%= link_to 'Reviews', product_reviews_path(product) %></td>
        <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Product', new_product_path %>

テーブルに、詳細ページへのリンクであるReviewsを追加します。
<%= link_to 'Reviews', product_reviews_path(product) %>

product_reviews_path/products/:product_id/reviews に対応します。引数で渡しているproductには、@products = Product.allを eachでひとつづつ抜き出したデータが入っています。

Railsの_pathヘルパーは引数に、モデル経由で取得した1つのデータを入れると、それに対応するパラメータのページへのリンクを作成してくれます。

また、最後のセルの中に4つのリンクが入るので、4つのセルを結合するよう、colspanの値を4にします。

実際の表示例は次のようになります(データ登録後)。

products > show.html.erb

製品詳細ページにレビューの一覧も合わせて表示するようにします。レビューが存在しない場合には、レビューのブロックを表示しないように条件分岐も入れます。

また、ページ下部に、製品レビュー作成ページへのリンクも追加しています。

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @product.name %>
</p>

<p>
  <strong>Price:</strong>
  <%= @product.price %>
</p>

<% if @reviews.count != 0 then %>
<div>
<p><strong>Reviews</strong></P>
    <table>
      <thead>
        <tr>
          <th>Rate</th>
          <th>Review</th>
        </tr>
      </thead>

      <tbody>
        <% @reviews.each do |review| %>
          <tr>
            <td><%= review.rate %></td>
            <td><%= review.review %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
</div>
<% end %>

<br>

<%= link_to 'Edit', edit_product_path(@product) %> |
<%= link_to 'Back', products_path %><br>
<%= link_to 'Write Review', new_product_review_path(params[:id]) %>

レビュー一覧を表示するロジック

テーブルに、詳細ページへのリンクであるReviewsを追加します。
<% if @reviews.count != 0 then %>から<% end %>までが追加した処理です。

@reviewsは、productsコントローラのshowアクションに追記した、@reviews = Review.where(product_id:params[:id])です。

if @reviews.count != 0 then は取得したレビューの数をカウントして、0以外(レビューがある場合)は、レビューの一覧を表示する条件分岐です。


レビュー作成ページへのリンク

末尾の<%= link_to 'Write Review', new_product_review_path(params[:id]) %>で対象製品のレビュー作成ページへのリンクを設置しています。

new_product_review_pathは、/products/:product_id/reviews/newに対応するルートです。

パラメータ:product_idとして、引数で現在のURLの:id部分を渡しています。(/products/:id の :id部分が渡されます)

実際の表示例は次のようになります(データ登録後)。

▼レビューがないページにアクセスした場合

レビューが存在しない場合は、レビューのブロック自体が表示されないようにしています。


reviewsのビューファイルの編集

reviewsのページにアクセスしたときに、表示するレビュー一覧や詳細ページ、また、レビューの編集・削除ページのビューファイルを編集します。

特に、scaffoldで生成したビューファイルのルーティングはネストする前の状態になっているので、_urlヘルパや_pathヘルパで指定するprefixは、新しいルーティングに合わせてすべて修正する必要があります。(修正しないとエラーになります)

▼変更前と変更後のprefix

変更前変更後パス
reviewsproduct_reviews/products/:product_id/reviews
new_reviewnew_product_review/products/:product_id/reviews/new
edit_reviewedit_product_review/products/:product_id/reviews/:id/edit
reviewproduct_review/products/:product_id/reviews/:id

また、それぞれで渡す必要があるパラメータにも注意が必要です。(:product_idだけか、:idも渡す必要があるか)


reviews > index.html.erb

各製品毎のレビュー一覧ページである、reviews > index.html.erbを以下のように編集します。

<p id="notice"><%= notice %></p>

<h1>Reviews for <%= @product['name'] %></h1>

<table>
  <thead>
    <tr>
      <th>Rate</th>
      <th>Review</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @reviews.each do |review| %>
      <tr>
        <td><%= review.rate %></td>
        <td><%= review.review %></td>
        <td><%= link_to 'Show', product_review_path(review.product_id, review.id)  %></td>
        <td><%= link_to 'Edit', edit_product_review_path(review.product_id, review.id) %></td>
        <td><%= link_to 'Destroy', product_review_url(review.product_id, review.id), method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Review', new_product_review_path %><br>
<%= link_to "#{@product['name']}の詳細ページに戻る", product_path(params[:product_id]) %><br>
<%= link_to 'Top (All Products)', products_path %>

このビューへは、reviewsコントローラのindexアクションから、@reviewsと@productが渡されます。

  def index
    @reviews = Review.where( product_id:params[:product_id] )
    @product = Product.find(params[:product_id])
  end

冒頭のh1には何のページかわかるように、@product['name']で製品名を表示します。

あとは、各レビューの横に表示する、詳細・編集・削除のlink_toのパスを修正します。例えば、詳細ページであれば、product_review_path(review.product_id, review.id) とします。

URIには、product_idとidの2つのパラメータが必要です。_pathの引数に2つ値を渡せば、入力順にパラメータにデータが渡ります。

最後に、レビューページから製品一覧や製品詳細ページにいけるようにリンクを設置します。

<%= link_to "#{@product['name']}の詳細ページに戻る", product_path(params[:product_id]) %><br>
<%= link_to 'Top (All Products)', products_path %>

ここでは日本語と英語が混在しているので、適宜、日本語化を行ってください。

tips

Rubyで文字列と変数を結合したい場合は、ダブルクオテーションで囲み、変数は#{ 変数 } とします。

※注意点:シングルクオテーションだと機能しません。


▼ 実際の表示例は次のようになります(データ登録後)

新規にレビューを追加した場合は、コントローラのcreateアクションで指定したnoticeが表示されます。


reviews > show.html.erb

次に、商品毎のレビューの詳細ページを編集します。

ここでは、製品名の表示と、末尾のリンクのパスの修正を行います。

<p id="notice"><%= notice %></p>

<p>
  <strong><%= @product['name'] %>のレビュー詳細</strong>
</p>

<p>
  <strong>Rate:</strong>
  <%= @review.rate %>
</p>

<p>
  <strong>Review:</strong>
  <%= @review.review %>
</p>

<%= link_to 'Edit', edit_product_review_path(@product['id'], @review['id']) %><br>
<%= link_to "Back to reviews for #{@product['name']}", product_reviews_path %>

このビューへは、reviewsコントローラのindexアクションから、@reviewと@productが渡されます。

どちらもbefore_actionで設定されている、set_reviewメソッドの実行結果です。

    def set_review
      @review = Review.find(params[:id])
      @product = Product.find(params[:product_id])
    end


▼ 実際の表示例は次のようになります(データ登録後)


reviews > _form.html.erb

reviewsの編集ページ(edit.html.erb)と新規登録ページ(new.html.erb)の中では、renderを使ってパーシャル _form.html.erbを呼び出しています。

この2つのページを正しく表示するためには、_form.html.erbの修正が必須です。

編集と新規登録を実行したときのメソッドとアクセス先は以下のようになります。

  • 新規登録: GETメソッド、 /products/:product_id/reviews/new
  • 編集:  PATCHメソッド、/products/:product_id/reviews/:id/edit

そのためには、form_withヘルパで生成されるformタグのアクションが、上記ルートになるようにする必要があります。

具体的には、form_withを以下のように修正します。

<%= form_with(model: [product, review]) do |form| %>
  (省略)

これで、コンパイルすれば、formaタグのactionの値が、productsの配下にreviewsが来るURIとなります。

↓ コンパイル例

<form action="/products/1/reviews/15" accept-charset="UTF-8" method="post"><input type="hidden" name="_method" value="patch"><input type="hidden" name="authenticity_token" value="A_EtsR7JhUdMtt1HKkW7UBUzTU64FkK3Jqz4vkJ27sMoVVH__Bgf0dVZ7h0S8X5tWHaQWt-MmFtkfgV0IKof2Q">


  <input value="1" type="hidden" name="review[product_id]" id="review_product_id">


  <div class="field">
    <label for="review_rate">Rate</label>
    <input type="number" value="5" name="review[rate]" id="review_rate">
  </div>

  (省略)

</form>

action="/products/1/reviews/15とすることができています。

また、type=”hidden”で生成されるinputタグのnameとidも、name="review[product_id]" id="review_product_id"のように指定したproductとreivewに対応したものになります。

注意点

form_with(model: [product, review])とするためには、呼び出し元の edit.html.erbnew.html.erbでproductとreviewの2つのデータを渡す必要があります。

この設定がされていないとエラーになります。

<%= render 'form', {review: @review, product: @product} %>

上記のようにすると、@reviewというデータをreviewという名前、@productというデータをproductという名前で渡します。


▼コードの全体像

<%= form_with(model: [product, review]) do |form| %>
  <% if review.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(review.errors.count, "error") %> prohibited this review from being saved:</h2>

      <ul>
        <% review.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>


  <%= form.hidden_field :product_id, value: product['id'] %>


  <div class="field">
    <%= form.label :rate %>
    <%= form.number_field :rate %>
  </div>

  <div class="field">
    <%= form.label :review %>
    <%= form.text_area :review %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>


reviews > edit.html.erb

_form.html.erbでデータをproductとreviewの2つのデータを受け取る準備ができたので、商品毎のレビューの編集画面を編集します。

ここでは、製品名の表示と、パーシャルに渡すデータにproductの追加、末尾のリンクのパスの修正を行います。

<h1>Editing Review for <%= @product['name'] %></h1>

<%= render 'form', {review: @review, product: @product} %>

<%= link_to 'レビュー詳細を見る', product_review_path(@review['product_id'], @review['id']) %><br>
<%= link_to "戻る(Product#{params[:product_id]}のレビュー一覧)", product_reviews_path(params[:product_id]) %>

renderでパーシャル _form.html.erbを読み込んでいます。 デフォルトではreviewsのデータしか渡さない状態なので、edit.html.erbと_form.html.erb 間で製品データ(product)を受け渡す必要があります。

renderを以下のようにしています。

<%= render 'form', {review: @review, product: @product} %>
tips

renderで指定したテンプレートに複数のデータを渡すときは、波カッコを使い{ キー名1: 値1, キー名2: 値2,,, }のようにします。


▼ 実際の表示例は次のようになります(データ登録後)


reviews > new.html.erb

edit.html.erbと同様に、new.html.erbを編集して、_form.erb.htmlにproductとreviewのデータを渡せるようにします。

<h1>New Review</h1>

<%= render 'form', { review: @review, product: @product} %>

<%= link_to 'Back', product_reviews_path %>

renderを以下のようにしています。

<%= render 'form', {review: @review, product: @product} %>


▼ 実際の表示例は次のようになります(データ登録後)


Jbuilderファイルの編集

scaffoldを使うと、JSON形式でデータを出力することができるJbuilderのファイルも生成されます。

あわせて読みたい

Jbuilderの詳しい使い方については下記をご参考ください。

【Rails】便利なJbuilderの使い方|ブラウザにJSON形式のデータ表示する方法

Jbuilderのファイルは、index.html.erbとshow.html.erbに対応する、index.json.jbuilderとshow. json.jbuilderがあります。

この2つのファイルは特に編集する必要がないのですが、中で呼び出しているJbuilderのパーシャル、_review.json.jbuilderでエラーが発生します。

このため、_review.jsonjson.jbuilderを次のように修正します。

json.extract! review, :id, :product_id, :rate, :review, :created_at, :updated_at
json.url review_url(review, format: :json)

↓ 修正後

json.extract! review, :id, :product_id, :rate, :review, :created_at, :updated_at
json.url product_review_url( params[:product_id], review, format: :json)

なお、エラー発生個所はjson.url review_url(review, format: :json)の部分です。これは、JSONデータに対象のURLを "url": "URL(絶対パス)"として追加するための処理です。

デフォルトでは_urlヘルパのprefixや引数で渡すデータがproductに対応していないので、これを修正します。


▼ 実際の表示例は次のようになります(データ登録後)

レビュー一覧の場合(URIの例:/products/1/reviews.json)

レビュー詳細の場合(URIの例:/products/1/reviews/15.json)

reviewsのJSONデータを表示することができました。


以上で、ネストしたresourcesを使った実際のプログラムが完成です。

日本語化やレイアウトの調整は行っていないので、適宜対応してみてください。



タイトルとURLをコピーしました