【Rails】scaffoldで作成したCRUDのshowページにscaffoldをネストする方法

Ruby on Railsアイキャッチ画像

scaffoldで作成したCRUDのshowページにscaffoldをネストしたときの変更点についてまとめました。リンクなど変更が必要な箇所が結構あり大変です。

やりたいこと

productにproduct_documentをネストする。

前提

  • productのCRUDは作成済
  • product_documentには、以下3つのカラムを作る
    name:string
    content:text
    public_level:integer

いざ実装へ

$ rails g scaffold ProductDocument user:references product:references name:string content:text public_level:integer

ここで、既にscaffoldで親のCRUDを作成しているためconflictが発生する。
上書き(overwrite)をする必要は無いため、noのnキーを入力

migrationファイルを確認し問題がなければ、

$ rails db:migrate

routes

resources :products do
  resources :product_documents
end

Models

rails g scaffoldの際にアソシエーションが作成されているはずのため、確認

class ProductDocument < ApplicationRecord
  belongs_to :user
  belongs_to :product
end

親側にはhas_manyを追加

class Product < ApplicationRecord
  has_many :product_documents, dependent: :destroy
end
class User < ApplicationRecord
  has_many :product_documents
end

Controllers

class ProductDocumentsController < ApplicationController
  before_action :set_product_document, only: %i[ show edit update destroy ]

  def index
    # @product_documents = ProductDocument.all
    @product = Product.where(id: params[:product_id]).first
    @product_documents = @product.product_documents.all
  end

  def show
  end

  def new
    # @product_document = ProductDocument.new
    @product = Product.where(id: params[:product_id]).first
    @product_document = @product.product_documents.build
  end

  def edit
  end

  def create
    # @product_document = ProductDocument.new(product_document_params)
    @product = Product.where(id: params[:product_id]).first
    @product_document = @product.product_documents.build(product_document_params)
    @product_document.user_id = current_user.id

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

  def update
    respond_to do |format|
      if @product_document.update(product_document_params)
        # format.html { redirect_to @product_document, notice: "Product document was successfully updated." }
        format.html { redirect_to [@product, @product_document], notice: "Product document was successfully updated." }
        format.json { render :show, status: :ok, location: @product_document }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @product_document.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @product_document.destroy
    respond_to do |format|
      # format.html { redirect_to product_documents_url, notice: "Product document was successfully destroyed." }
      format.html { redirect_to product_product_documents_url, notice: "Product document was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    def set_product_document
      # @product_document = ProductDocument.find(params[:id])
      @product = Product.where(id: params[:product_id]).first
      @product_document = @product.product_documents.where(id: params[:id]).first
    end

    def product_document_params
      params.require(:product_document).permit(:user_id, :product_id, :name, :content, :public_level)
    end
end

scaffoldのデフォルトを#でコメントアウトしています。

ここがポイント!

@product = Product.where(id: params[:product_id]).first

.firstは不要では?と思ったけど、無くすとエラーになる。idがproduct_idにマッチするのは1個しかないけど、where句は条件にマッチした全件を取得するというメソッドのため、エラーになる。

ならばと、、find_byを使って以下のように書くこともできた。

@product = Product.find_by(id: params[:product_id])

Views

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

<h1>Product Documents</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Content</th>
      <th>Public level</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @product_documents.each do |product_document| %>
      <tr>
        <td><%= product_document.name %></td>
        <td><%= product_document.content %></td>
        <td><%= product_document.public_level %></td>
        <!-- <td><%= link_to 'Show', product_document %></td> -->
        <td><%= link_to 'Show', product_product_document_path(@product, product_document) %></td>
        <!-- <td><%= link_to 'Edit', edit_product_document_path(product_document) %></td> -->
        <td><%= link_to 'Edit', edit_product_product_document_path(@product, product_document) %></td>
        <!-- <td><%= link_to 'Destroy', product_document, method: :delete, data: { confirm: 'Are you sure?' } %></td> -->
        <td><%= link_to 'Destroy', product_product_document_path(@product, product_document), method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<!-- <%= link_to 'New Product Document', new_product_document_path %> -->
<%= link_to 'New Product Document', new_product_product_document_path %>

rails g scaffold時にresourcesで作成したカラムのフォームが自動生成されるためその部分は削除する。

<h1>Editing Product Document</h1>

<!-- <%= render 'form', product_document: @product_document %> -->
<%= render 'form', { product: @product, product_document: @product_document } %>

<!-- <%= link_to 'Show', @product_document %> | -->
<%= link_to 'Show', product_product_document_path(@product, @product_document) %> |
<!-- <%= link_to 'Back', product_documents_path %> -->
<%= link_to 'Back', product_product_documents_path %>

<h1>New Product Document</h1>

<!-- <%= render 'form', product_document: @product_document %> -->
<%= render 'form', { product: @product, product_document: @product_document } %>

<!-- <%= link_to 'Back', product_documents_path %> -->
<%= link_to 'Back', product_product_documents_path %>

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

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

<p>
  <strong>Content:</strong>
  <%= @product_document.content %>
</p>

<p>
  <strong>Public level:</strong>
  <%= @product_document.public_level %>
</p>

<!-- <%= link_to 'Edit', edit_product_document_path(@product_document) %> | -->
<%= link_to 'Edit', edit_product_product_document_path(@product, @product_document) %> |
<!-- <%= link_to 'Back', product_documents_path %> -->
<%= link_to 'Back', product_product_documents_path %>

indexと同様に、rails g scaffold時にresourcesで作成したカラムのフォームが自動生成されるが、不要のためその部分は削除する。

実はpathの後の、(@product, @product_document)の順番はどちらでもいい。

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

      <ul>
      <% product_document.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

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

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

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

こちらも同様に、rails g scaffold時にresourcesで作成したカラムのフォームが自動生成されるが、不要のためその部分は削除する。

親の変更点

product_documentのindexに飛ぶためのlinkを追加

<%= link_to '資料', product_product_documents_path(@product.id) %>

まとめ

そもそものテーブルの命名が悪く、product_product_documentなどといった、冗長なコードとなってしまっていること失礼しました。

コメント