おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

ActiveRecord復習その3

ようやくリレーションについて。

belongs_to, has_one, has_manyの基本

今日読んだ範囲では特に新しいことはなかった。

has_and_belongs_to_many

has_and_belongs_to_many(habtm)は利用シーンがすごく限られていて、覚えていてもあんまり使えないんじゃないかなーという気がする。結合テーブルを作ったら、そこに情報をいろいろ付加したくなるし、リレーションの片方を検索してそこからもう片方をfindするような時は、結合テーブルに検索情報の列を付加してあげた方がクエリの発行回数が少なくなるし。同様にhas_many :hoge, :through => :hogehgoeもあんまり使いどころがない気がする。


この話題に関してはかなりもやもやしている最中なので、いずれもう少し掘り下げて書きたいと思います。

アソシエーションの拡張

アソシエーションの拡張っていうのは下記のように、has_many*1にブロックを渡してあげて、その中でメソッドを定義することで実現できます。

class User < ActiveRecord::Base
  has_many :readings
  has_many :articles, :through => :readings do
    def rated_at_or_above(rating)
      find :all, :conditions => ["rating >= ?", rating]
    end
  end
end

通常はhas_manyを指定するだけで、下記のようなメソッド複数定義されるようになるのだけど、それを上記のコードで拡張できるという訳です。

  user.articles.build(:name => name)

アソシエーションの拡張を複数のモデルで使い回したいときは、モジュールにメソッドを記述して:extendパラメータで指定してあげるといい。

  has_many :articles, :extend => RatingFinder

単一テーブル継承

親と子の関係がある複数のモデルを一つのテーブルで管理する方法。
まず、テーブルにtypeという名前の列を作ります。

create_table :people do |t|
  t.column :type, :string

  # 全てのモデルに共通の列
  t.string :name
  t.string :email

  # Customerモデル専用列
  t.decimal :balance

  # Employeeモデル専用列
  t.integer :reports_to
  t.integer :dept
end

その次にこんな感じでモデルを作ります

class Person < ActiveRecord::Base
  # ...
end
class Customer < Person
  # ...
end
class Employee < Person
  # ...
end
class Manager < Employee
  # ...
end

こうすると、それぞれのモデルをDBに保存するときに、railsが自動でtype列を入力してくれます。そして下記のようにfindを使うと、railsが自動でtype列を参照して、type列に対応するクラスとして取得してくれます。

  Manager.create(:name => "manager")
  manager = Person.find_by_name("manager")
  p manager.class #=> Manager

これは利用頻度が結構ありそうだし、モデルをスマートに管理できそうなので覚えておこうっと。

type列を設定する時に気をつけること

単一テーブル継承でテーブルの列名に使われている「type」は、Rubyの組み込みのメソッド(.classの別名)なので下記のように使うとおかしいことになるみたい。

person.type = "Manager"

type列を設定するときは、下記のようにハッシュ形式で設定してあげるといいみたいです。

person[:type] = "Manager"

ポリモーフィックアソシエーション

単一テーブル継承は、各モデルで共通する列がある程度あれば使えるけど、共通する列が少ないときは使いにくい。そういうときにはポリモーフィックアソシエーションを使うといいらしい。(2008/7/14追記と修正を行いました)

「catalog_entryにはarticle, sound, imageのいずれか一つのコンテンツが含まれる」といった場合に、それらをまとめるresourceという仮の抽象モデル(のようなもの)を定義してポリモーフィックアソシエーションを設定してあげるとスマートに書ける。具体的には下記のようにする。

まず、テーブルに抽象モデルの外部キーとタイプを設定する。

create_table :catalog_entries do |t|
  t.string :name
  t.datetime :acquired_at
  t.integer resource_id
  t.string resource_type
end

次に、各々のモデルのテーブル定義をする。

create_table :articles do |t|
  t.column :content, :text
end

create_table :sounds do |t|
  t.column :content, :binary
end

create_table :images do |t|
  t.column :content, :binary
end

そして、各々のモデルには下記のように書いてあげる。

class CatalogEntry < ActiveRecord:Base
  belongs_to :resource, :polymorphic => true
end

class Article < ActiveRecord:Base
  has_one :catalog_entry, :as => :resource
end

class Sound < ActiveRecord:Base
  has_one :catalog_entry, :as => :resource
end

class Image < ActiveRecord:Base
  has_one :catalog_entry, :as => :resource
end

このようにすると、catalogentry.resouceにArticle, Sound, Imageを設定したときに、railsが自動的にresource_idとresource_typeを入れてくれる。取得時にもrailsがresource_typeを見て、適切なモデルオブジェクトとして取得してくれる。

  a = Article.new(:content => "hoge")
  c = CatalogEntry.new(:name = "entry", :acquired_at => Time.now)
  c.resource = a
  c.save!
  p c.resource_type #=> "Article"

参考書籍

RailsによるアジャイルWebアプリケーション開発 第2版

RailsによるアジャイルWebアプリケーション開発 第2版

*1:has_oneやbelongs_toでもできるみたい