カテゴリー:
Rails
タグ:
 Rails model view polymorphic form global_id

このエントリーをはてなブックマークに追加
更新日時:
2016年11月16日(水)
作成日時:
2016年11月16日(水)

前の記事 / 次の記事

参考ページ:
http://www.opendrops.com/rails/2015/01/30/rails-globalid-polymorhphic-select-form/
Ruby on Rails 4.2 リリースノート

参考ページのglobal_idの使い方が目から鱗。
以下写経。

例えばこういうモデル

class Book < ApplicationRecord
  has_many :reviews
end

class Music < ApplicationRecord
  has_many :reviews
end

class Review < ApplicationRecord
  belongs_to :resource, polymorphic: true
end

例えばレビュー側からモデルを作成したくて次のようなビューをつくった時、
まず次の記述は できない

= form_for @review do
  %p
    = f.label :resource
    = f.grouped_collection_select :resource, [Book, Music], :all, :model_name, :id, :name
  %p
    = f.submit

Rails的にresourceをselectの対象にしたら上手くいきそうな気がするけどできない。
Railsの掟ではselectする対象はあくまでresource_idである必要がある。

仕方ないのでselectの対象をresource_idにして次のようなビューをつくった時、

= form_for @review do
  %p
    = f.label :resource_type, value: "Book"
    = f.radio_button :resource_type, "Book"

    = f.label :resource_type, value: "Music"
    = f.radio_button :resource_type, "Music"
  %p
    = f.label :resource_id
    = f.grouped_collection_select :resource_id, [Book, Music], :all, :model_name, :id, :name
  %p
    = f.submit

問題がある

BookとMusicで同じidがあった場合(というか当然ある)、
ユーザーがBookとMusicを間違って選択した場合、間違いなのかどうか分からない。

ラジオボタンでBookを選んで、idが5であるBookを選んだ場合と、
ラジオボタンでBookを選んで、idが5であるMusicを選んだ場合の判別が不可能。

そこで、セレクトボックスをBookとMusicで分けて間違いを判別ができるようにしてみると、

= form_for @review do
  %p
    = f.label :resource_type, value: "Book"
    = f.radio_button :resource_type, "Book"

    = f.label :resource_type, value: "Music"
    = f.radio_button :resource_type, "Music"
  %p
    = f.label :resource_id
    = f.collection_select :resource_id, Book.all, :id, :name
  %p
    = f.label :resource_id
    = f.collection_select :resource_id, Music.all, :id, :name
  %p
    = f.submit

同じ属性のセレクトボックスが2つ発生してしまいそのままだとバグるので、
別途JavaScript等で制御が必要になって怠い。

Reviewに book_id, music_id等の仮想属性を設定してビューからは
仮想属性を受け取るという方法もなくはない。

class Review < ApplicationRecord
  attr_reader :book_id, :music_id
  belongs_to :resource, polymorphic: true

  def book_id=(book_id)
    self.resource_id = book_id if resource_type == "Book"
  end

  def music_id(music_id)
    self.resource_id = music_id if resource_type == "Music"
  end
end

= form_for @review do
  %p
    = f.label :resource_type, value: "Book"
    = f.radio_button :resource_type, "Book"

    = f.label :resource_type, value: "Music"
    = f.radio_button :resource_type, "Music"
  %p
    = f.label :book_id
    = f.collection_select :book_id, Book.all, :id, :name
  %p
    = f.label :music_id
    = f.collection_select :music_id, Music.all, :id, :name
  %p
    = f.submit

上手くいきそうだ。
しかし、本末転倒でこんなことするならそもそもpolymorphicにせずに素直に次のようにすればいい。
しかも将来 belongs_to の対象が増える度に修正しなければならず怠すぎる。

class Review < ApplicationRecord
  belongs_to :book
  belongs_to :music
end

そこで、global_idを使ってシリアライズしてしまうと綺麗に嵌まる。

class Review < ApplicationRecord
  belongs_to :resource, polymorphic: true

  def global_resource
    self.resource.to_global_id if resource.present?
  end

  def global_resource=(resource)
    self.resource = GlobalID::Locator.locate resource
  end
end

= form_for @review do
  %p
    = f.label :global_resource
    = f.grouped_collection_select :resource_id, [Book, Music], :all, :model_name, :id, :name
  %p
    = f.submit

この時、セレクトボックス内の値は次のように
resource_typeとresource_id両方の情報を持った状態でシリアライズされているため、
これをデシリアライズすることで、BookのidなのかMusicのidなのか判別できる。

<select name="review[global_resource]" id="option_global_resource">
  <option value="">選択するのです</option>
  <optgroup label="Book">
    <option value="gid://appname/Book/1">たのしいRuby</option>
    <option value="gid://appname/Book/2">RailsによるアジャイルWEBアプリケーション開発</option>
    <option value="gid://appname/Book/3">パーフェクトRuby on Rails</option>
  </optgroup>
  <optgroup label="Music">
    <option value="gid://appname/Music/1">君が代</option>
    <option value="gid://appname/Music/2">蛍の光</option>
    <option value="gid://appname/Music/3">仰げば尊し</option>
  </optgroup>
</select>

これでモデル同様ビューもplymorphicなビューとなった。