ちょっと話題の記事

[Rails] STI(Single Table Inheritance)でコントローラも一つに纏める

2014.05.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Rails で STI(Single Table Inheritance)を使った時の、コントローラの実装をどうするか?という事について書いてみたいと思います。

STI(単一テーブル継承)とは

オブジェクト指向の基本概念として、あるクラスを元にサブクラスを定義する継承があります。
この継承は、プログラミング言語ですとと予め機能が提供されていることが多いですが、 RDB ですとテーブル構成によって表現する場合があります。 *1
今回使う STI は、一つのテーブル内に継承関係にあるクラスのカラムを全て持ってしまう方法です。

例として Player クラスを継承した Footballer クラスと Cricketer クラスがあるとします。

class

親クラスに共通のプロパティ、子クラスがそれぞれのプロパティを持っていますが、これを STI で表現すると以下になります。

table

テーブルのレコードがどちらの型にあたるのかのカラム(type)を持っていて、それぞれの型が使うカラムのみにデータが入ります。

Rails ではこの STI がデフォルトでサポートされていて、テーブル定義とクラスの継承によって、簡単に使うことが出来ます。

  • type カラムを保持したテーブルあること。 *2
  • 上記のテーブルにマッピングされたモデルの派生クラスがあること。
  • これで STI が実現出来るようになります。

    モデルの実装

    最初に type カラムを持つように migrate ファイルの作成と、親となるモデル Player を作成します。

    $ bundle exec rails g model player name:string club:string 'batting_average:decimal{4,3}’ type:string
    # db/migrate/20140514063117_create_players.rb
    class CreatePlayers < ActiveRecord::Migration
      def change
        create_table :players do |t|
          t.string :name
          t.string :club
          t.decimal :batting_average, precision: 4, scale: 3
          t.string :type
    
          t.timestamps
        end
      end
    end
    [/ruby]
    <p><strong>Player</strong></p>
    
    <p>次に Player モデルを継承した <tt>Footballer</tt>、<tt>Cricketer</tt> も作成します。</p>
    <p><strong>Footballer</strong></p>
    
    
    <p><strong>Cricketer</strong></p>
    
    
    
    <p>デフォルトですと <tt>type</tt> カラムにクラス名の表記(<strong>Footballer</strong> のようにキャメルケースで)保存されます。<br/>
    今回は小文字で登録したかったため、player クラスに以下のように追記しました。</p>
    
    
    <h3>動作確認</h3>
    <p>DBマイグレートをした後に、動きを確認してみましょう。</p>
    
    <p> <tt>type</tt> カラムにそれぞれのクラス名が登録され、検索条件にも自動的に含まれていますね。</p>
    
    <h2 id="toc-1">コントローラの実装</h2>
    <p>次にコントローラの実装です。<br/>
    <tt>PlayersController</tt> を作成して <tt>FootballersController</tt>、<tt>CricketersController</tt> を子クラスとして定義してもいいのですが、やっぱり Ruby なので DRY に書きたいですよね。<br/>
    <tt>PlayersController</tt> でそれぞれのリクエストを処理したいと思います。</p>
    
    <p>※シンプルな例にするためにレスポンスを json で返すようにしています。<br/>
    <tt>type</tt> パラメータに <tt>Footballer</tt> または、<tt>Cricketer</tt> が指定されると、<tt>player_class</tt> メソッドは、そのモデルクラスを返します。</p
    <p><tt>routes.rb</tt> には以下のように定義しました。</p>
    
    # config/routes.rb
    Rails.application.routes.draw do
    
      resources :footballers, controller: :players, type: 'Footballer', except: %i(new edit), defaults: {format: :json}
      resources :cricketers, controller: :players, type: 'Cricketer', except: %i(new edit), defaults: {format: :json}
    
    end

    footballerscricketers の controller として players を指定し、type に子モデルのクラス名を指定します。

    上記の定義によって、以下のようなルーティングが指定出来ました。

    $ bundle exec rake routes
         Prefix Verb   URI Pattern                Controller#Action
    footballers GET    /footballers(.:format)     players#index {:format=>:json, :type=>"Footballer"}
                POST   /footballers(.:format)     players#create {:format=>:json, :type=>"Footballer"}
     footballer GET    /footballers/:id(.:format) players#show {:format=>:json, :type=>"Footballer"}
                PATCH  /footballers/:id(.:format) players#update {:format=>:json, :type=>"Footballer"}
                PUT    /footballers/:id(.:format) players#update {:format=>:json, :type=>"Footballer"}
                DELETE /footballers/:id(.:format) players#destroy {:format=>:json, :type=>"Footballer"}
     cricketers GET    /cricketers(.:format)      players#index {:format=>:json, :type=>"Cricketer"}
                POST   /cricketers(.:format)      players#create {:format=>:json, :type=>"Cricketer"}
      cricketer GET    /cricketers/:id(.:format)  players#show {:format=>:json, :type=>"Cricketer"}
                PATCH  /cricketers/:id(.:format)  players#update {:format=>:json, :type=>"Cricketer"}
                PUT    /cricketers/:id(.:format)  players#update {:format=>:json, :type=>"Cricketer"}
                DELETE /cricketers/:id(.:format)  players#destroy {:format=>:json, :type=>"Cricketer"}

    動作確認

    定義されたルーティングに対して URL にアクセスして確認してみます。

    $ bundle exec rails s
    $ curl http://localhost:3000/footballers | jq .
    [
      {
        "updated_at": "2014-05-14T06:57:59.000Z",
        "created_at": "2014-05-14T06:57:59.000Z",
        "batting_average": "0.354",
        "club": null,
        "name": "David",
        "id": 1
      }
    ]
    $
    $ curl http://localhost:3000/cricketers | jq .
    [
      {
        "updated_at": "2014-05-14T06:58:06.000Z",
        "created_at": "2014-05-14T06:58:06.000Z",
        "batting_average": null,
        "club": "marylebone",
        "name": "Emily",
        "id": 2
      }
    ]

    それぞれのモデルのデータを返していますね。同じように登録や更新を試すと、モデル毎に動作するのが確認出来ると思います。

    まとめ

    STI を使った際のコントローラを一つに纏める方法でした。ルーティングでコントローラの入り口を2つに分けることで、コード量は少なくとてもシンプルに実現出来たと思います。ちなみに今回試した環境は、Ruby 2.1.1 と Rails 4.1.1 でした。

    参考資料

    http://thibaultdenizet.com/tutorial/single-table-inheritance-with-rails-4-part-1/

    脚注

    1. RDB によっては継承機能を実装しているものもあります。
    2. カラム名は変更することが出来ます。