3
2

More than 1 year has passed since last update.

多対多に関連付けられた要素を登録する

Posted at

はじめに

 前回の記事からしばらく期間が空いてしまった。全く何もやっていないということはなく、ポートフォリオアプリのコードを粛々と書いていた。

 一対多は前回説明したので、今回は多対多の要素を登録するやりかたを記す。

やりたいこととDB構造

 開発中の日報アプリには振り返り機能がある。あらかじめ目標を設定しておき、振り返りの段階でそれが完了したかどうかをチェックする。以下の流れになる。

step1. 事前に目標を複数登録
step2. 振り返る目標を複数選択
step3. それぞれの目標に対して進捗を振り返り
step4. 総括のコメントを追加

必要となるテーブルはPlan/Review/ReviewItemの三つ。
Plan → ReviewItem(多) / Review → ReviewItem (多)の多対多の構造をとっている。

というわけで振り返り機能を実装するときに多対多の要素の登録が必要になった。
それぞれは以下のようになっている。

Planモデル

app/models/plan.rb
class Plan < ApplicationRecord
  belongs_to :user
  belongs_to :genre
  has_many :review_items
  accepts_nested_attributes_for :review_items
end
db/migrate/2021************_create_plans.rb
class CreatePlans < ActiveRecord::Migration[6.0]
  def change
    create_table :plans do |t|
      t.references :user, null: false, foreign_key: true
      t.references :genre, null: false, foreign_key: true
      t.string :name, null: false
      t.datetime :deadline, null: false
      t.string :status, null: false
      t.timestamps
    end
  end
end

Reviewモデル

app/models/review.rb
class Review < ApplicationRecord
  belongs_to :user
  has_many :review_items
  accepts_nested_attributes_for :review_items
end
db/migrate/2021************_create_reviews.rb
class CreateReviews < ActiveRecord::Migration[6.0]
  def change
    create_table :reviews do |t|
      t.string :content, null: false
      t.references :user, null: false, foreign_key: true
      t.datetime :reviewed_on, null: false
      t.timestamps
    end
  end
end

ReviewItemモデル

app/models/review_item.rb
class ReviewItem < ApplicationRecord
  belongs_to :review
  belongs_to :plan
end
db/migrate/2021************_create_review_items.rb
class CreateReviewItems < ActiveRecord::Migration[6.0]
  def change
    create_table :review_items do |t|
      t.references :plan, null: false, foreign_key: true
      t.references :review, null: false, foreign_key: true
      t.timestamps
    end
  end
end

再度説明

テーブルの中身も合わせて上記の1-4の流れを説明すると以下のようになる。

step1. 事前に目標を複数登録 (Plan)
step2. 振り返る目標を複数選択 (Plan-ReviewItemのひもづけ)
step3. それぞれの目標に対して進捗を振り返り (Planのstatus 変更)
step4. 振り返りの日時/コメントを追加 (Reviewのcontent, reviewed_on 登録)

3と4は同時に行う。

step1.目標の複数登録

 割愛する。seedデータで入れ込んでもいいし、あとから1個ずつ追加してもいい。

step2.目標選択機能の実装

 振り返る目標を選択する画面を追加したい。Reviewコントローラの中に新しいアクションを追加しよう。既存のRESTfulな7つのアクションではなく。

 ルーティングから考える。

ルーティング

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root "plans#index"
  resources :plans
  resources :reports
  resources :report_items, only: [:index, :destroy]
  resources :genres, except: :show
  resources :reviews do
    collection do
      get 'select_plan'
    end
  end
end

resourcesの中にコレクションルーティングを追加した。コレクションとはidを伴わないアクションのことである。今回は目標選択を新規に行うのでidは必要ない。
参考↓

これによって ルートに select_plan_reviews が追加された。

$ rails routes | grep reviews
                  select_plan_reviews GET    /reviews/select_plan(.:format)                                                           reviews#select_plan
                              reviews GET    /reviews(.:format)                                                                       reviews#index
                                      POST   /reviews(.:format)                                                                       reviews#create
                           new_review GET    /reviews/new(.:format)                                                                   reviews#new
                          edit_review GET    /reviews/:id/edit(.:format)                                                              reviews#edit
                               review GET    /reviews/:id(.:format)                                                                   reviews#show
                                      PATCH  /reviews/:id(.:format)                                                                   reviews#update
                                      PUT    /reviews/:id(.:format)                                                                   reviews#update
                                      DELETE /reviews/:id(.:format)                                                                   reviews#destroy

コントローラー

コントローラーも修正する。自分のアカウントのPlanを全てもってくる。(本当はフィルターをかけて一部だけ抽出を実施しているが、今回は説明しない。)

app/controllers/reviews_controller.rb
class ReviewsController < ApplicationController
  before_action :authenticate_user!

# 中略

  def select_plan
    @plans = Plan.where(user_id: current_user.id)
  end

# 中略

end

view

viewはreviews/select_plan.html.erb を追加する。ルーティング先だ。

app/views/reviews/select_plan.html.erb
<h1>振り返り登録-目標選択</h1>
<%= form_with url: new_review_path, method: :get, local: true do |form| %>
  <% @plans.each do |plan| %>
    <p>
    <%= check_box_tag 'checked_plan[]',"#{plan.id}" %>
    目標: <%= plan.name %>
    締め切り:<%= plan.deadline %>
    ステータス:<%= plan.status %>
    ジャンル:<%= plan.genre_id %>
    </p>
  <% end %>
  <%= form.submit "登録" %>
<% end %>

check_box_tag について

 check_box_tag はその名の通り、チェックボックスをつくるヘルパーだ。ドキュメントを読むとモデルに関係ないチェックボックスを生成する、とある。この引数の設定が難しかったので説明する。

<%= check_box_tag 'checked_plan[]',"#{plan.id}" %>

↓
(submitを押した後 渡されたparams)
 params
=> <ActionController::Parameters {"checked_plan"=>["9", "7"], "commit"=>"登録", "controller"=>"reviews", "action"=>"new"} permitted: false>

 このように、第一引数の最後に[]をつけると、選択したPlanの plan.idを配列としてパラメータに渡してくれる。つけないとチェックを入れた最後のひとつしか表示されない。

step3-step4.目標に対しての振り返り

 選択した目標に対して振り返りを行う。具体的には目標モデルのstatusの変更(状況に応じて締め切りの変更も行う)と、全体の振り返りコメントの登録をする。

コントローラ(newアクション)

app/controllers/reviews_controller.rb
class ReviewsController < ApplicationController
  before_action :authenticate_user!

#中略

  def new
    @review = Review.new
    selected_plan_ids = select_plan_params[:checked_plan].map(&:to_i)
    @plans = Plan.where(id: selected_plan_ids)
  end

#中略

  private

  def select_plan_params
    params.permit(checked_plan: [])
  end
end

newアクションの中身はわりと簡単である。チェックをいれたPlanのidの配列はselect_plan_params でもってこれるようにしている。

view

app/views/reviews/new.html.erb
<h1>振り返り登録</h1>
<%= form_with model: @review, local: true do |form| %>
  <p>振り返り日:<%= form.date_field :reviewed_on %></p>
    <% @plans.each do |plan| %>
      <%= form.fields_for 'plans[]', plan do |f| %>
      <p>
      目標: <%= plan.name %>
      ジャンル:<%= plan.genre_id %>
      締め切り:<%=l plan.deadline %>
      ステータス:<%= plan.status %>
      </p>
      <p>変更後の締め切り:<%= f.date_field :deadline %></p>
      <p>変更後のステータス:<%= f.select :status, ["進行中","中止","完了"] %></p>
    <% end %>
  <% end %>
  <p>コメント:<%= form.text_field :content %></p>
  <%= form.submit "登録" %>
<% end %>

すこしややこしいことをしている。

fields_forについて

 fields_forを使うと、ひとつのフォームで異なるモデルを編集することができる。今回はreviewモデルとplanモデルを編集する。
第一引数で渡すパラメータ名を指定している。第二引数のplanは直前で繰り返しの処理をしている |plan|のことである。

下記のように第一引数の末尾に[]をつけない場合、複数のplanモデルの編集ができない。

<%= form.fields_for 'plans', plan do |f| %>

渡されるパラメータ
( "plans"=>{"deadline"=>"2021-11-25", "status"=>"中止"} となっている。)

 params
=> <ActionController::Parameters {
"authenticity_token"=>"****",
 "review"=>{
   "reviewed_on"=>"2021-11-23",
   "plans"=>{
      "deadline"=>"2021-11-25", "status"=>"中止"
    }, 
    "content"=>"----"
 }, 
 "commit"=>"登録", "controller"=>"reviews", "action"=>"create"
} permitted: false>

indexオプションを付与しても[]と同様のことができる。

<%= form.fields_for 'plans', index: plan.id do |f| %>
もしくは
<%= form.fields_for 'plans[]', plan do |f| %>
 params
=> <ActionController::Parameters {
"authenticity_token"=>"****",
 "review"=>{
   "reviewed_on"=>"2021-11-23",
   "plans"=>{
      "4"=>{"deadline"=>"2021-11-25", "status"=>"中止"}, 
      "9"=>{"deadline"=>"2021-11-27", "status"=>"完了"}
    }, 
    "content"=>"----"
 }, 
 "commit"=>"登録", "controller"=>"reviews", "action"=>"create"
} permitted: false>

コントローラ(createアクション)

new.html.erbで登録を押すと、createアクションを実行する。そんなに大きなことはやっていない。パラメータの指定については、binding.pryを挟みながらparamsになにが入っているかを確認して行った。
一発ではわからなかった。

app/controllers/reviews_controller.rb
class ReviewsController < ApplicationController
  before_action :authenticate_user!

#中略
  def create
    review = Review.new(review_params)
    review.user_id = current_user.id
    review.save

    param_plans = params.require(:review)[:plans]
    plan_keys = param_plans.keys
    item = param_plans.values

    plan_keys.each_with_index do |id, i|
      @plan = Plan.find(id)
      @plan.update!(item[i])
      review.review_items.create!(plan_id: id)
    end
  end

#中略

  private
  def review_params
    params.require(:review).permit(:content, :reviewed_on)
  end

end

これで登録は完了した。

 おわりに

 まだバリデーションの追加や、reviewの編集時の操作など紹介していないことがいろいろあるが、これで一旦終わりにする。またややこしいことがあったら書く。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2