多対多のリレーションを考えたときに、親モデルを作成すると同時に子モデルも作成したいときがあります。ブログにカテゴリーをつけたいときとか。
accepts_nested_attributes_forを使用したやり方がよく紹介されていますが、評判が良くないといった書き込みがあったり、個人的に挙動がわかりにくかったのでaccepts_nested_attributes_forを使用しない方法を記載しました。
ターゲットはあくまでも初心者ですので、ていねいにコメントを多めにしながら書きました。
#サンプルとしてuser(ユーザー)とjob(仕事)の多対多の関係を考えます
userモデルとjobモデルが中間テーブルuser_jobで多対多のリレーションを持つとします。
##とりあえずuserを作成します。
userをつくりましょう。まずモデルから
$rails g model user name:string
$rails db:migrate
今回は名前だけ持たせます。
コントローラをつくります。userをつくるだけのシンプルなものです。
$rails g controller users
def new
@user = User.new
@users = User.all
end
def create
User.create(user_params)
redirect_to new_user_path
end
private
def user_params
params.require(:user).permit(:name)
end
<h1>ユーザー登録</h1>
<%= form_for @user do |f| %>
<%= f.label :name %>
<%= f.text_field :name%>
<%= f.submit "登録" %>
<% end %>
<% @users.each do |user| %>
<%= "#{user.id}:#{user.name}" %><br>
<% end %>
resources :users
#user登録と同時に中間テーブル(user_jobs)を作成することを考えてみる
##とりあえずjobモデルをつくりましょう
名前だけを持たせます。
$rails g model Job name:string
$rails db: migrate
migrateを忘れずに。
jobをいくつかつくりたいのですが、formを作るのが面倒なのでコンソールでjobを作成します。
> Job.create name: "farmer"
> Job.create name: "fisherman"
今回はfarmerとfishermanを作成しました。
##中間テーブルuser_jobsを作成する
$rails g model UserJob user:references job:references
$rails db:migrate
user:references job:references
とすることでuser_idとjob_idを自動生成してくれます。
##has_manyとbelongs_toを加える
#これを書き加える
has_many :user_jobs
has_many :jobs, through: :user_jobs
has_manyを書き加えることでuser.jobs.build
やuser.user_jobs
といったメソッドが使えるようになる。belongs_to
は中間テーブル作成時に初めからあるはずです。
##ここでユーザー登録と同時にuser_jobsも登録することを考えます。
<h1>ユーザー登録</h1>
<%= form_for @user do |f| %>
<!--ここはいつも通りの@userのname部分のform-->
<%= f.label :name %>
<%= f.text_field :name%>
<!--ここが中間テーブルuser_jobsを保存させるためのform
パラメータを扱いやすくするためfields_forでネストさせます-->
<%= f.fields_for :job do |j| %>
<!--jobは複数あるのでeachで全て取り出してチェックボックスにします-->
<% Job.all.each do |job| %>
<%= j.label job.name %>
<%= j.check_box job.name,{}, true, false %>
<% end %>
<% end %>
<%= f.submit "登録" %>
<% end %>
<!--この部分は保存したuserとjobを確認しているだけなので分かれば何でもいいです。-->
<% @users.each do |user| %>
<% if user.jobs.first.nil? %>
<%= "#{user.id}:#{user.name}" %><br>
<% else %>
<%= "#{user.id}:#{user.name}:" %>
<% user.jobs.each do |job| %>
<%= "#{job.name}" %>
<% end %>
<br>
<% end %>
<% end %>
f.fields_forをつかって中間テーブル作成用のformをuser作成formにネストしています。
ここでは、あらかじめ登録したjobをチェックボックスで選択してuserに登録するようにします。
formでネストすることによって
params[:user][:job]= {"farmer"=>"true", "fisherman"=>"true"}
のようにチェックボックスでの値が取り出せます。
##あとはコントローラで中間テーブルを保存する
def create
created_user = User.create(user_params)
#checkboxで受け取ったパラメータはhashになっているので一つづつ取り出して保存する
unless params[:user][:job].nil? #nilガード
params[:user][:job].each do |key, value|
if value == false #チェックされていない場合はスキップする
next
else
job_id = Job.find_by(name: key).id #keyで探して保存するだけです
user_job = UserJob.create(user_id: created_user.id, job_id: job_id)
end
end
end
redirect_to new_user_path
end
private
def user_params
params.require(:user).permit(:name)
end
##終わり
チェックボックスに入れてuser登録するとこんな感じになりましたでしょうか?
accepts_nested_attributes_forを使ってみたり、f.collection_check_boxesをつかってみたりしましたが、いまいち挙動がつかめず気持ち悪かったので、きちんと自分で書いてみました。
リファクタリングできたり、fatコントローラである点は置いておいてください。
参考になればうれしいです。