はじめに
UserモデルがProfileモデルを1つ持っているという状況を考えます。
(User has_one Profile のような状況)
usersテーブルのカラムにプロフィール情報を追加して管理するより、ユーザー認証のためのロジックとプロフィール情報のデータを分けて管理したいと思ったということですね。
そして、この記事では「1つのフォームで2つのモデルに同時に値を送る(かつ、ProfileがUserにネストしている)」という要件を実装していきます。
導入
ますはUserモデルの準備をします。
$ rails new devise_app # => アプリ作成
$ rails db:create # => データベースの作成
$ rails g devise:install # => deviseの導入(Gemfileに(gem 'devise')必要)
$ rails g devise:view # => deviseのビューの作成
$ rails g devise User # => Userモデルの作成
$ rails g devise:controllers users # => usersコントローラの作成
これでUserモデルに関する準備は完了です。
Profileモデルも作ります。
Userモデルから呼び出せるように、外部キー制約を付けておきます。(user_idカラムを持つ)
$ rails g model Profile name:string user:references
現在、サインアップ画面(/users/sign_up)は以下のようになっています。
もちろんまだProfileに関するフォームはありません。
以降の本編で、実装していきます。
本編
UserモデルとProfileモデルの関連付け
Userモデルを以下のようにprofileと関連づけます
class User < ApplicationRecord
has_one :profile
accepts_nested_attributes_for :profile
# ...
end
「accepts_nested_attributes_for :profile」によって、Userモデルを通して、Profileの属性の値をDBに保存することが出来ます。
この関連付けの詳しい挙動を知りたい方は以下のアドレスが参考になるかもしれません。
「ActiveRecord::NestedAttributes::ClassMethods」
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
ProfileモデルはUserモデルにbelongs_toさせましょう。 (今回の場合は、外部キー制約を付けてmigrateしているので、既に定義されています。)
class Profile < ApplicationRecord
belongs_to :user
end
コントローラーでの準備
予め、controllers/users/registration_controller.rbをコマンドで作成していますので、そこに移動しましょう。
まずはインスタンスがサインアップ画面に送られるよう、コードを書きます。
(あとで、/users/sign_upパスへのGETリクエストがここのコントローラーを通るようにルーティングを設定します。)
def new
@user = User.new
@profile = @user.build_profile
end
「build_profile」は、Userモデルに定義した「has_one :profile」によって、Userモデルのインスタンスメソッドとして使えるようになっているメソッドです。
「@profile = Profile.new」としても、ビューで@userにネストした@profileを参照することができません。
ルーティングの準備
先ほど準備したコントローラーを通るように、ルーティングを追加します。
Rails.application.routes.draw do
devise_for :users, controllers: { registrations: 'users/registrations' }
end
ビューの準備
devise/registations/new.html.erbに「f.fields_for :profile do ~ end」を加えます。
これで、@profileを@userにネストさせながら同じフォームで値を送信できます。
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
# ...
<%= f.fields_for :profile do |f| %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<% end %>
# ...
ビューを見てみよう
やりました! 「Name」のフォームが追加されていますね。
これで、@userの情報を送るのと同時に、@profileの情報も送れます。
あとはこの情報を受け取るためのストロングパラメータを設定しましょう。
ストロングパラメータの設定
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [profile_attributes: [:name] ])
end
Userモデルに定義した「accepts_nested_attributes_for :profile」のおかげで、profile_attributesをキーとして、@profileの属性の中身を受け取ることが出来ます。
(今回だと、nameの値が受け取れます。)
この値はProfileモデルにてバリデーションチェックを受けます。
ユーザー登録してみよう
「sign up」を押すと…。
Started POST "/users" for 127.0.0.1 at 2019-03-10 22:26:39 +0900
Processing by Users::RegistrationsController#create as HTML
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"7KormqpmzDyJF+Yva+T84vho9oZ7OgKNYiXOFEzw9D2sv8m0rX3EtjTChHCR0YJhtugatTH5lLvTdsxh7J8qrg==",
"user"=>{"email"=>"user@example.com",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]",
"profile_attributes"=>{"name"=>"user"}},
"commit"=>"Sign up"}
パラメータがちゃんと値を受け取り、ユーザー作成が完了します!
下から2行目に、先程ストロングパラメーターで受け取るキーとして許可した"profile_attributes"がありますね。
User Create (0.4ms) INSERT INTO `users` (`email`, `encrypted_password`, `created_at`, `updated_at`) VALUES ('user@example.com', '$2a$11$7siXRQ2mONK2FR4C0fB40uZbs30IZKSCH9/D2/JHx9ICZqRxf7hIK', '2019-03-10 13:26:39', '2019-03-10 13:26:39')
Profile Create (0.3ms) INSERT INTO `profiles` (`name`, `user_id`, `created_at`, `updated_at`) VALUES ('user', 2, '2019-03-10 13:26:39', '2019-03-10 13:26:39')
(1.0ms) COMMIT
もしUserモデルかProfileモデル、どちらかのバリデーションに引っかかったらROLLBACKします。
おわりに
以上で、deviseで2つのモデルに同時に値を送る方法について解説を終わります。
実装してる内容自体はそこまで複雑じゃないですが、ググっても明確にまとめてある所がなかったり数年前だったりの古い情報が多かったので記事書きました。
ざっくりした内容ですが誰かの助けになればいいな、と思います。