2006.10.09

Ruby Rails

Railsで単一テーブル継承(Single Table Inheritance)

ActiveRecord以外のORマッパーはどうなのかよく知らないのですが、Rails(というかActiveRecord)では、DB上の一つのテーブルを複数のモデルで共有する「単一テーブル継承(Single Table Inheritance)」というものが存在します。今日はこの単一テーブル継承についてちょっと紹介します。

ここでは種々のメッセージ機能を単一テーブル継承で実現する方法を例に、単一テーブル継承について紹介します。今回想定するメッセージ機能はSNSなんかでよく使われるであろう以下の3つです。

  • 一般メッセージ:NormalMessage
  • 招待メッセージ:InvitationMessage
  • お問い合わせ:InquiryMessage

それでは、すべてのモデルのデータ保存先となるMessageテーブルの定義から始めましょう。

まず単一テーブル継承をActiveRecordで使う為には、DBにtypeというcolumnを用意してやります(これ必須!!)。Messageテーブルの定義はこんな感じです。

create_table :messages do |t|
  # 全モデル共通で使うカラム
  t.column :title, :string
  t.column :body, :text
  t.column :created_at, :datetime
  t.column :updated_at, :datetime
  t.column :type, :string  # 必須
  # モデルによって使ったり使わなかったりするカラム
  t.column :from_user_id, :integer
  t.column :to_user_id, :integer
  t.column :to_user_address, :string
  t.column :from_user_address, :string
  t.column :key, :string
  t.column :finished, :boolean, :default => false
end

ここでtypeというカラムは、モデルのクラス名を保存する場所であり、こいつがActiveRecordでSingle Table Inheritanceを使うときの肝になります。それ以外は全モデルで使うカラム、モデルによって使ったり使わなかったりするカラムを、すべてMessageテーブルに定義します。

次にModel側の実装に移ります。まず、すべてのメッセージモデルの元となるMessageクラスを作ります。

class Message < ActiveRecord::Base
 
  belongs_to :from, :class_name => 'User', :foreign_key => :from_user_id
  belongs_to :to, :class_name => 'User', :foreign_key => :to_user_id
 
  def send_mail
  end
 
  def after_create
    send_mail
  end
 
  validates_presence_of :body
end

MessageクラスはActiveRecord::Baseを継承しているので、DBのmessageテーブルと対応づけられます。

ここではすべてのメッセージが、DBに保存された後で、各モデルでオーバーライドされたsend_mailメソッドによってメール送信されるように設計されています。こうすると@message.saveとすれば自動でメール送信までされるので、コントローラ側の実装が楽ちん♪

またMessageクラスで定義されたassociationおよびvalidationは、それ以降のすべてのモデルで有効になります。

次に一般メッセージで利用するNormalMessageクラスを作ります。

class NormalMessage < Message
 
  def send_mail
    ApplicationMailer.send_normal_message self
  end
 
  validates_presence_of :title, :to_user_id, :from_user_id
end

ここで重要なのは、NormalMessageクラスはActiveRecord::BaseではなくMessageクラスを継承しているということです。これにより、NormalMessageクラスのデータはmessageテーブルにtype='NormalMessage'として保存されます。

また一般メッセージは、Messageクラスへの追加機能として、titleとto_user_id、from_user_idを必須にし、ApplicationMailerクラスのsend_normal_messageメソッドでメールを送信するように定義されています。

次に招待メッセージで利用するInvitationMessageクラスを作ります。

class InvitationMessage < Message
 
  def send_mail
    ApplicationMailer.send_invitation_message self
  end
 
  def before_create
    create_key self  # ここで何かしらkeyを作る。ここでは書かない。
  end
 
  validates_presence_of :from_user_id, :to_user_address, :key
end

InvitationMessageもMessageを継承しています。また招待キーを発行するため、before_createの中でkeyを作る何らかの処理をします。InvitationMessageをメール送信するのは、ApplicationMailerクラスのsend_invitation_messageメソッドになっています。

あとはお問い合わせですね。

class InquiryMessage < Message
 
  def send_mail
    ApplicationMailer.send_inquiry_message self
  end
 
  validates_presence_of :from_user_address
end

もう説明はいらないですよね。

単一テーブル継承を使うのに必要なことはこれだけです。あとは

Message.find :all

SELECT * FROM MESSAGE;

のようなSQLが実行され、

NormalMessage.find :all

では

SELECT * FROM MESSAGE WHERE type='NormalMessage';

のようなSQLが実行されます。

コントローラ側で

Message.find :all

としておいて、ビューでは

<%= render :partial => @message.class.name.downcase %<

とすれば、

  • _message.rhtml
  • _normalmessage.rhtml
  • _invitationmessage.rhtml
  • _inquirymessage.rhtml

というテンプレートを用意するだけで、各種メッセージの表示もできちゃいます。
注)Railsでは@object.class.nameでクラス名を取得できます。

単一テーブル継承をうまく使えば、モデルだけじゃなくてコントローラやビューまで見やすく整理されます。すばらしい♪

この機能、メッセージングだけじゃなくて、一般会員/有料会員/管理者というような複数種類のユーザを管理するときなんかにも使えますね。

ただ一つ注意しないといけないことは、テーブルが一つに集中するので、DB分散とか考えだすとちょっとややこしくなるという。。。

Single Table Inheritanceの恩恵は、このブログが参考になる?
I.T.System | ありえない仕様変更

ちなみにPHPでもできるらしい。
CakePHPのおいしい食べ方: Single Table Inheritance を CakePHP で実現するには