1対多の関係で同時に作成/更新する
1対多の関係で"1"を作成(更新)する際に"多"も一緒に作成(更新)する。
例。
- UserとAccountがある。
- User作成時にUserに関連するAccountを作成したい。
- Accountは同時に複数作成したい。
以下サンプル。migrationファイルは以下のような感じ。
便宜上indexは省略。また、accountsはhas_secure_password
を使うものとする。
# db/migrate/20150215225710_create_users.rb class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :username t.timestamps end end end # db/migrate/20150215225801_create_accounts.rb class CreateAccounts < ActiveRecord::Migration def change create_table :accounts do |t| t.string :account t.string :password_digest t.string :remember_token t.integer :user_id t.timestamps end end end
この状態でrake db:migrate
をする。
そしてmodelを以下のようにする。validationは省略。
# app/models/user.rb class User < ActiveRecord::Base has_many :accounts accepts_nested_attributes_for :accounts, allow_destroy: true, reject_if: all_blank end # app/models/account.rb class Account < ActiveRecord::Base belongs_to :user has_secure_password end
ここでポイントが3つ。作成/更新作業を行うmodel(この例ではUser)に
accepts_nested_attributes_for
reject_if
allow_destroy
を記述する。これによりUserフォーム上で
- 指定した関連テーブルも同時に作成/更新する
- 指定した条件下においてAccountのvalidationを無効にする
- 更新時に関連するAccountを削除する
ことができるようになる。
なお、この例ではreject_if: :all_blank
としているので
Accountフィールド内が全て空の場合のみAccountのvalidationを無効にしている。
これにより関連先を同時に作成したくない場合にも対応ができる。
次にController。
# app/controllers/users_controller.rb class UsersController < ApplicationController before_action :set_user, only: [:edit, :update, :destroy] def new @user = User.new @user.accounts.build end def create @user = User.new(user_params) if @user.save flash[:success] = "登録しました" redirect_to users_url else render 'new' end end def index @users = User.all end def edit end def update if @user.update(user_params) flash[:success] = "更新しました" redirect_to users_url else render 'edit' end end def destroy @user.destroy redirect_to index_url end private def set_user @user = User.find(params[:id]) end def user_params params .require(:user) .permit( :username, account_attributes: [ :id, :account, :password, :password_confirmation, :_destroy ] ) end end
ここでのポイントはズバリstrong_parameters。
- 関連先のカラムも更新出来るようにするため
accounts_attributes
をpermitに追加し配列でaccount側のフィールドを渡す。 :id
はUser更新フォームで関連するAccountも同時に更新するために必要。これがないとrailsは関連先のidが分からないため表示されず更新ができない。:_destroy
はUser更新フォームで関連するAccountの削除をする場合に使う。booleanで値を受け取りtrueの場合には関連先を削除してくれる。
他、newアクションではaccountも同時に作成するため@userのほかに@user.accounts.build
をする。
これによりnewアクションで関連するaccountのフォームが生成できるようになる。
次にview側。
# app/views/users/new.html.erb <%= form_for(@user, html: { class: "form-horizontal"}) do |f| %> <%= render 'fields', f: f %> <div class="form-group"> <span class="col-sm-offset-2 col-sm-10"> <%= f.submit "登録", class: "btn btn-primary" %> </span> </div> <% end %>
# app/views/users/_fields.html.erb <div class="form-group"> <%= f.label :username, class: "control-label col-sm-2" %> <span class="col-sm-10"> <%= f.text_field :username, class: "form-control" %> </span> </div> <%= f.fields_for :accounts do |f| %> <div class="form-group"> <%= f.label :account, class: "control-label col-sm-2" %> <span class="col-sm-10"> <%= f.text_field :account, class: "form-control" %> </span> </div> <div class="form-group"> <%= f.label :password, class: "control-label col-sm-2" %> <span class="col-sm-10"> <%= f.password_field :password, class: "form-control" %> </span> </div> <div class="form-group"> <%= f.label :password_confirmation, class: "control-label col-sm-2" %> <span class="col-sm-10"> <%= f.password_field :password_confirmation, class: "form-control" %> </span> </div> <% if @user.persisted? %> <div class="form-group"> <%= f.label :_destroy, class: "control-label col-sm-2" %> <span class="col-sm-10"> <%= f.check_box :_destroy %> </span> </div> <% end %> <% end %>
fields_for
で関連するaccountを作成できる。@user.persisted
な場合のみ:_destroy
フィールドを用意しtrueな場合にはaccountを削除できるようにしている。
もしaccountを同時に複数作りたい場合にはfields_for以下のフィールドを切り出してrender
で呼び出しajaxで処理すれば良い。
最後にi18n関連。
# config/locales/ja.yml ja: activerecord: attributes: user: username: ユーザー名 user/accounts: account: アカウント password: パスワード password_confirmation: パスワード(確認用)
これが分からず結構ハマった。
account:
配下に書いてもUserフォーム内では適用されないのでuser/accounts:
として定義する必要がある。