1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

1対多の要素を1つのviewで登録する/個人開発のつづき

Last updated at Posted at 2021-10-17

はじめに

 ずっとコードを書いていたが、それだけではだいじなことを忘れそうな気がしたので、ここに記録しておこう。今、個人開発で日報アプリをつくろうとしている。ユーザーは日々の記録を書ける。各作業のジャンル、作業名、何時間したかさいごにその日のコメントをつける。

 ログインした後、reportを登録する。reportひとつに対してreport_itemを複数登録できる。以下のようにしたい。

例:
ユーザー名:satou
登録日: 2021/10/21
report_item 1
  ジャンル: Ruby
  やったこと: メソッド復習
    作業時間: 1時間

report_item 2
  ジャンル: 英語
  やったこと: TOEIC対策
    作業時間: 2時間

report_item 3
  ジャンル: 筋トレ
  やったこと: ランニング
    作業時間: 0.5時間

全体のコメント:
  よくできた。

これを実現するにはどうすればいいのだろう。ひとつずつやっていくことにした。

モデルを考える

まずはモデルとその関係性を考えないといけない。必要なモデルはUser,Report,Report_itemそしてGenreだ。

User
 -メールアドレス
 -パスワード
  1対多: reports/genres
  
Report
 -コメント
 -登録日
 1対多: report_items

Genre
 -ジャンル名
 1対多:report_items

Report_item
 -内容
 -何時間したか
 

 ユーザーは複数とReportと複数のGenreを持ち、Reportは複数のReport_itemを持つ。Genreも複数のReport_itemを持つ。個々のバリデーションは考えながら追加しよう。

モデルを作る

 そうやってモデルを作っていく。はじめにやるのがUserモデルだ。これにはログイン機能が必要だが、私はdeviseというgemを使ってそれを実装する。このdeviseの兼ね合いがあり、あとから追加すると面倒臭かった記憶がある。

 ここを参考にしながらgemを導入してモデルをつくった。rails g model ではなく rails g devise をつかう。

$ rails g devise:install
$ rails g devise User

 あとは普通につくる。モデルの関連付けは以下のようになる。

app/models/user.rb
class User < ApplicationRecord
  has_many :reports, dependent: :destroy
  has_many :genres, dependent: :destroy
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
app/models/report.rb
class Report < ApplicationRecord
  belongs_to :user
  has_many :report_items, dependent: :destroy
  accepts_nested_attributes_for :report_items
end
app/models/report_item.rb
class ReportItem < ApplicationRecord
  belongs_to :report
  belongs_to :genre
end
app/models/genre.rb
class Genre < ApplicationRecord
  has_many :report_items,dependent: :restrict_with_exception
  belongs_to :user
end

accepts_nested_attributes_forがだいじ

 report.rb にサラッと記述されている accepts_nested_attributes_forだが、これがかなり重要だ。

最初は単一のオブジェクトを編集していただけのシンプルなフォームも、やがて成長し複雑になります。たとえば、Personを1人作成するのであれば、そのうち同じフォームで複数の住所レコード(自宅、職場など)を登録したくなるでしょう。後でPersonを編集するときに、必要に応じて住所の追加・削除・変更が行えるようにする必要もあります。

親のモデルに accepts_nested_attributes_for を記述することで関連しているモデルも編集することができるようになる。

ルーティングを整える

 ルーティングはわりと簡単だ。resourcesを使うだけ。

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 "reports#index"
  resources :reports
end

コントローラーとをビュー作る

 これがちょっと厳しかった。今回はreportのindex/new/createアクションだけ紹介する。
rails g controller reports でコントローラーとviewファイルが生成されるはずだ。

indexとnew アクション

indexアクションとnewアクションを書く。

app/controllers/reports_controller.rb
class ReportsController < ApplicationController
  before_action :authenticate_user!
  def index
    @reports = Report.where(user_id: current_user.id)
  end

  def new
    @report = Report.new
    3.times { @report.report_items.build }
    @select_genre = Genre.where(user_id: current_user)
  end

  def create

  end

end

indexアクションはそこまで問題ではない。Reportを自分が登録したものだけ引っ張ってきている。
before_action :authenticate_user! はdeviseによって使用できるメソッドであり、ログインしていない状態ではログインページへリダイレクトされる。

newアクションではreportを新しく作り、その子要素を3つ作っている。これは新規投稿画面でreport_itemを一気に3つまで登録したいためだ。

index/newのビュー

 indexのビューは以下のようになる。

app/views/reports/index.html.erb
<h1>日報一覧</h1>
<% @reports.each do |report| %>
  <p>
  日報ID:<%= report.id %>
  </p>
  <p>日付:<%= report.reported_on %></p>
  <% report.report_items.each do |item| %>
    <p>
    ID:<%= item.id %>
    ジャンルID:<%= item.genre_id %>
    やったこと:<%= item.content %>
    時間:<%= item.work_hours %>
    </p>
  <% end %>
  コメント: <%= report.content %>
  </p>
  <hr>
<% end %>

いたって普通だ。コントローラーの@reportsを呼び出し、繰り返しで中の要素を書き出している。ジャンルIDを普通の名前表示にしたりとかは、また別の話。

newのビューは以下のようになった。

app/views/reports/new.html.erb
<h1>日報登録</h1>
<%= form_with model: @report, local: true do |f| %>
  <p>登録日:<%= f.date_field :reported_on , value:Date.today%></p>
  <%= f.fields_for :report_items do |item| %>
    <p>ジャンル:<%= item.collection_select :genre_id, @select_genre, :id, :name, :include_blank => true %></p>
    <p>やったこと:<%= item.text_field :content %></p>
    <p>作業時間:<%= item.text_field :work_hours %></p>
  <% end %>
  <p>コメント:<%= f.text_field :content %></p>
  <%= f.submit "登録" %>
<% end %>

ここが少し大変だった。form_withとfields_forの使い方を調べるのが難しかった。Railsのバージョンが新しくなってから、form_forとform_tagではなくform_withを利用することが推奨され始めた。

 form_with  のあとに登録するモデルを書く。繰り返しの構文のように |f| を指定してやる。f.date_fieldf.text_field を設定することでいろんなスタイルのフォームを作成できる。

 あとはfileds_for を使うとreportsとは関係のないモデルへの登録も可能となる。ここで|item| をつかって繰り返しをすると、さきほどつくった3つの子要素のフォームが表示される。

このあたりは公式のgithubのコメント欄にいろいろ書いてある。rails ドキュメントより記載が豊富だ。

createアクション

 これが本当に面倒くさかった。なにをどう登録するかがわからなかったのだ。まずはbinding.pryを挟んで、newアクション後のparamsになにが渡っているのか確認した。

app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 略

  def create
   binding.pry
  end

end

以下のようなものが入っていた。

pry(#<ReportsController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"qyEey4ZqOElVMESUggFdPNOjsgsmDDK7yLZDtwuxv9yxVsI8kPkzspTbkVhyYyeppK1qR/mhG7ed1sjDR95wDA==", "report"=><ActionController::Parameters {"reported_on"=>"2021-10-17", "report_items_attributes"=>{"0"=>{"genre_id"=>"1", "content"=>"a", "work_hours"=>"3"}, "1"=>{"genre_id"=>"", "content"=>"", "work_hours"=>""}, "2"=>{"genre_id"=>"", "content"=>"", "work_hours"=>""}}, "content"=>"test"} permitted: false>, "commit"=>"登録", "controller"=>"reports", "action"=>"create"} permitted: false>

なんとなくわかった。

このまま利用するとセキュリティ的にあぶないとどこかに書いてあった。

railsにはマスアサイメントといって複数の項目を一括で更新/登録ができる機能が備わっている。しかし、これは渡されるハッシュを読み取っているだけなので、一般ユーザが管理者権限がないと更新できない項目もいぢれてしまう!という可能性もでてくるらしい。

ストロングパラメータを使って、設計者側から利用できるパラメータを指定すべき、らしい。

ネストされているので少し工夫が必要だ。

params.require(:report).permit(:content, :reported_on, report_items_attributes: [:content, :genre_id, :work_hours, :id])

上記のようにした。report_items_attributes には:idも必要だった。これをストロングパラメータとしてコントローラーに追記する。

app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 略

  def create
   binding.pry
  end

# 略

  private

  def report_params
    params.require(:report).permit(:content, :reported_on, report_items_attributes: [:content, :genre_id, :work_hours, :id])
  end
end

あとは3つの子要素のうち、1つだけ/2つだけ登録したいというときの処理を考える必要がある。ストロングパラメータにしたreport_paramsにはなにがはいっているのか。

report_params
=> <ActionController::Parameters {
 "content"=>"eee", "reported_on"=>"2021-10-17",
 "report_items_attributes"=>
  <ActionController::Parameters {
    "0"=><ActionController::Parameters {"content"=>"a", "genre_id"=>"2", "work_hours"=>"1"} permitted: true>, 
    "1"=><ActionController::Parameters {"content"=>"", "genre_id"=>"", "work_hours"=>""} permitted: true>, 
    "2"=><ActionController::Parameters {"content"=>"", "genre_id"=>"", "work_hours"=>""} permitted: true>
   } 
 permitted: true>
} 
permitted: true>

なるほど。いろいろ入っていた。"1","2"のなかみがcontent=""になっているので、これをうまいこと除去してやればよさそうだ。

そうして以下のようになった。

app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 略

  def create
    para = report_params[:report_items_attributes]
    first_key = para.keys.first
    first_value = para.values.first
    para.reject! { |_key, value| value[:content] == "" }
    para[first_key] = first_value unless para.key?("content")
    formatted_para = report_params
    formatted_para[:report_items_attributes] = para
    report = Report.new(formatted_para)
    report.user_id = current_user.id
    report.save!
  end
# 略

end

中央のpara.rejecet!でcontentが空白になっているものは除去するようにした。3つの子要素すべのcontentが空白だとバリデーションではじくようになっている。

# できた

 やっとひとつの機能ができた。なんだかうれしい気分になった。でも次に行った実装はもっと面倒くさかった。多対多の要素の登録である。

 それはまた別のお話。
↓やっていく github
https://github.com/kyokucho1989/simple-record

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?