シングルテーブル継承のコレクションをrenderする


Railsには「設定より規約」の発想で、いろいろ便利な機能がありますが、複数組み合わせるとちょっと予想していなかった挙動をしました。

シングルテーブル継承とは

Rubyに限らず、オブジェクト指向プログラミングの上で継承は欠かせない仕組みですが、RDBMSと組み合わせるとなると少し複雑となってしまいます。

ただ、RailsのActiveRecordには標準で「シングルテーブル継承(STI)」という仕組みが用意してあって、

  • テーブルは親クラスで定義して、すべての子クラスが同じ列を持つ
  • (デフォルトでは)テーブルのtype列を、子クラスの識別に使う1
  • ActiveRecordからコレクションとして取ってきた時にも、きちんと子クラスとして取れる

という感じになります。

コード例

# マイグレーション
create_table :smart_phones do |t|
  t.string :type
  t.integer :storage
  t.integer :frequency
  t.string :maker
  t.string :version
  t.timestamps, null: false
end

# app/models/smart_phone.rb
class SmartPhone < ActiveRecord::Base
end

# app/models/i_phone.rb
class IPhone < SmartPhone
  # 特有の処理も書ける
  default_value_for(:maker) { 'Apple' }
end

# app/models/android_phone.rb
class AndroidPhone < SmartPhone
end

コレクションのrender

ビューで使うrenderにはいろんな機能がありますが、コレクションをまとめて表示することもできます。

たとえばDigitalBookモデルに対して、コレクションの変数名をモデルのキャメルケース複数形(@digital_books)としておけば、render @digital_booksと書くだけで、「@digital_booksの各要素を'digital_book'というパーシャルで表示する。各要素はパーシャル内からdigital_bookとして参照できる」という動作を実現してくれます。

STIコレクションをまとめてrender

STIのコレクションをまとめて表示するような場合、子クラス同士がSTIの性質上同じような構造となるので、「全部まとめて同じパーシャルでいい」というような場面もあるかもしれません(実際、そういう「よく似たもの同士」を入れるのに向いた構造ですし)。

実際に、上のSmartPhoneのようなデータ構造を作って、_smart_phoneパーシャルを用意した上でrender @smart_phonesとしてみたところ、「_android_phoneのパーシャルが見つかりません」とエラーになってしまいました。どうやら、STIのコレクションをrenderすると、各クラスごとのパーシャルを呼んでくれるようでした。

ということで、全部同じでいいような場合は、render collection: @smart_phones, partial: 'smart_phone'のように明示的に指定するのが必要だったようです。なお、「本来のモデルに合わせてリンク先を変えたい」ということなら、polymorphic_pathを使えば、モデルとアクションに合わせてアドレスを設定してくれます。


  1. 別な目的でtypeを使いたい場合、設定変更しておく必要があります。