ブログのような何か

Rails関連の備忘録だったり、個人的なメモだったり・・

Railsのアソシエーションと複数登録 ~新規登録~

前回の続き

登録画面と確認画面を作成し、新規登録まで出来るようにする。

ルーティングの作成

newとedit関連をまとめて設定

  resource :profile, only: [:new, :create, :edit, :update] do
    post :confirm, on: :collection
  end

postでconfirmへ遷移するよう設定する。
プロフィールは一つしかないので、IDを指定する必要が無い。
resource :profileとし、confirmにもon: :collectionを指定

コントローラーの作成

  def new
    @profile = current_user.build_profile
    @profile.game_careers.build
    @profile.possess_games.build
  end

  def confirm
    @profile = current_user.build_profile(profile_params)
  end

  def create
    @profile = current_user.build_profile(profile_params)
    if @profile.valid?
      @profile.save!
      redirect_to root_path
    end
  end

current_user.build_profile で ProfileにUserIDを入れることが出来る。
同時に○○.buildをしておくと、初期状態で入力欄が表示される。
(今回は動的に入力欄を追加出来るようにするのでなくてもOK。)

ビューの作成

今回は確認画面を挟むので、フォームでPostした場合にConfirmへ飛ぶように指定する。

<%= form_for profile, url: confirm_profile_path, method: :post do |f| %>
 -省略-
  <%= f.submit '確認画面へ', class: 'btn btn-primary' %>
<% end %>

Confirm画面は入力した内容を表示する。
hiddenをセットし忘れると値を渡すことができないので注意。
項目が多い場合は↓のように書くとスッキリする。

 -省略-
<%= form_for @profile, url: profile_path, as: :profile do |f| %>
  <% [:email, :tell, :prefecture_id, :city ].each do |attr| %>
    <%= f.hidden_field attr %>
  <% end %>
  <%= f.submit '登録する', class: 'btn btn-primary' %>
<% end %>

複数登録の実装

ゲーム経験と所有ゲーム機を複数登録出来るようにする。

Association先のモデル保存には accepts_nested_attributes_for を使う。
※ accepts_nested_attributes_for はそのうち消える運命らしい。。
FormObjectを使うと良いらしいので、今度試してみる。

model/profile.rb

# 追加
 accepts_nested_attributes_for :game_careers, :possess_games, allow_destroy: true

allow_destroy: trueを追加することで"_destory"がtrueのときに削除できるようになる。

controller/profiles_controller.rb

 # ストロングパラメータに○○_attributesを追加する。
 # _destroyも受け取れるようにする。
  def profile_params
    params.require(:profile).permit(
      :email, :tell, :prefecture_id, :city,
      game_careers_attributes: [:id, :profile_id, :name, :_destroy],
      possess_games_attributes: [:id, :profile_id, :game_console_id, :_destroy]
    )
  end

入力フォームにはnested_form_fieldsを使用する。

Gemfile

# 追加してbundle install
gem 'nested_form_fields'

app/assets/javascript

# 追加
//= require jquery
//= require jquery_ujs
//= require nested_form_fields

jqueryjquery_ujsを使用するので追加。
逆だと動かないので順番にも注意。

views/profile/_form.html.erb

<%= form_for profile, url: confirm_profile_path, method: :post do |f| %>
 -省略-
 # テキストボックスに入力
 <%= f.nested_fields_for :game_careers, wrapper_tag: :div do |career| %>
  <%= career.text_field :name, class:'form-control', id:'careers', placeholder: 'careers'%>
  <%= career.remove_nested_fields_link 'Delete', class: 'btn btn-danger', role: 'button' %>
 <% end %>
 <%= f.add_nested_fields_link :game_careers, 'Add new', class: 'btn btn-primary', role: 'button' %>
 # セレクトボックスから選択。選択肢はGameConsoleから取得
 <%= f.nested_fields_for :possess_games, wrapper_tag: :div do |games| %>
  <%= games.collection_select :game_console_id,GameConsole.all, :id, :name, class:'form-control', placeholder: 'games'%>
  <%= games.remove_nested_fields_link 'Delete', class: 'btn btn-danger', role: 'button' %>
 <% end %>
 <%= f.add_nested_fields_link :possess_games, 'Add new', class: 'btn btn-primary', role: 'button' %>
<% end %>

nested_fields_for内のremove_nested_fields_linkが削除ボタン、add_nested_fields_linkがフォーム追加ボタン。
追加したフォームをdivで囲むnested_fields_forにwrapper_tag: :divを追加すればOK。

views/profile/confirm.html.erb

  # 項目表示
  ~ 省略 ~
    <% @profile.game_careers.each do |c| %>
      <%= c.name %>
    <% end %>
    <% @profile.possess_games.each do |g| %>
      <%= g.game_console.name %>
    <% end %>

  # hidden追加
  ~ 省略 ~
  <%= f.fields_for :game_careers do |career| %>
    <%= career.hidden_field :name %>
    <%= career.hidden_field :_destroy %>
  <% end %>
  <%= f.fields_for :possess_games do |games| %>
    <%= games.hidden_field :game_console_id %>
    <%= games.hidden_field :_destroy %>
  <% end %>

表示とhiddenを追加。hiddenには'_destroy'も忘れず追加すること。
登録だけならこれで問題無いが、修正時に問題があるのでそのときに修正。

これで入力から登録までが完了。
次は編集画面を実装する。

Railsのアソシエーションと複数登録 ~モデルの準備~

Facebook等からユーザー情報を持ってくる場合は、個人的には別テーブルに詳細情報をもたせたい。
詳細情報が各項目必ず一つであれば詳細テーブル内に入れてしまえば良いが、
例えば経歴だったり資格だったり、複数持つ情報の場合は詳細テーブルだけでは事足りない。

そんな時のアソシエーションの備忘録 (・ω・)

はじめに

OAuth認証でログインしてきた人に追加で必要な情報を入力させる画面を想定してみる。
今回はそこに持っているゲーム機とゲーム経験を登録させる。
このあたりのデータはなんでも良いが、特定のリストから選択と自由入力項目を複数同時に登録出来るような入力画面を作成する。

要件定義

ざっくりと大体↓こんな感じ。

  • Userは一つのProfileを持つ
  • Profileは複数のゲーム機を持つ
  • ゲーム機名称は別テーブルで持たせて、選択させる
  • Profileは複数のゲーム経験を持つ こちらは自由入力

モデルを作成する

まずは必要なモデルを作る。

class User < ApplicationRecord
 # OAuth認証を想定して最低限の情報で
end
class Profile < ApplicationRecord
 # 入力してほしい情報はすべてここに集約
end
class GameCareer < ApplicationRecord
 # ゲーム経験
end
class GameConsole < ApplicationRecord
 # 所持しているゲーム機 
end
class PossessGame < ApplicationRecord
# 所持してるゲーム機とプロフィールをつなげる中間テーブル
# 今回は繋げるだけなのでIDしか用意し無いが、例えば購入日等あればここに乗せても良い。
end

アソシエーションを組む

ユーザーとプロフィール
class User < ApplicationRecord
  has_one :profile, dependent: :destroy
end
class Profile < ApplicationRecord
  belongs_to :user
end

ユーザーは必ず1つ(2以上はありえない)のプロフィールを持つのでhas_oneで繋げる。
belogns_to側にIDを入れる必要があるので、Userをhas_one,Profileをbelongs_toとする。

プロフィールとゲーム経験
class Profile < ApplicationRecord
  has_many :game_careers, dependent: :destroy
end
class GameCareer < ApplicationRecord
  belongs_to :profile
end

プロフィールは複数のゲーム経験を持つので、has_manyで繋げる。

プロフィールと持っているゲーム機
class Profile < ApplicationRecord
  has_many :possess_games, dependent: :destroy
end
class PossessGame < ApplicationRecord
  belongs_to :profile
  belongs_to :game_console
end
GameConsole < ApplicationRecord
 has_many :possess_games
end

中間テーブルが出て来るが基本的に同じ。やってることは名称のマスタ化。(いわゆる第二正規化)
これでProfile - PossessGame と PossessGame - GameConsoleは繋がったが、
このままだとプロフィールから直接持っているゲーム機を取得するのが面倒なので
ProfileとGameConsoleを繋げる。

class Profile < ApplicationRecord
  -省略-
  has_many :games, through: :possess_games, source: :game_consoles
end
GameConsole < ApplicationRecord
  -省略-
  has_many :profiles, through: :possess_games
end

through: :possess_gamesとすると、中間テーブルを介してデータを取得出来る。
また、has_manyの後にaliasを定義し、sourceでモデル(テーブル名)を指定すると@profile.○○と別名で呼べるので、モデル名が無駄に長いときやわかりにくい時に便利。

一応直接繋げない場合でも↓のように呼び出すことが可能。

@profile.possess_games.map{|game|game.game_console}


ここまでで、アソシエーションが完了。
seedで適当なデータを登録してConsoleを叩くと、データが取得できる。
アソシエーションの詳細はreflect_on_all_associationsメソッドで確認できるので、モデルから何のデータが呼べるのか確認する場合は、nameを取得すると一発。

pry(main)> Profile.reflect_on_all_associations.map(&:name)
=> [:user, :game_careers, :possess_games, :games]

>>> Profileから以下の4つが呼び出せる。
@profile.user 
@profile.game_careers
@profile.possess_games
@profile.games

次は登録画面を作成していく。