今さらRails3メモ - その5: Model Association -
まずは設計以前の話から。(というか設計は語れません。)
あと、Rails ガイド読むならこのエントリ要らない。
Ruby on Rails Guides: A Guide to Active Record Associations
基本概念
用語
DBMS 用語としては entity の relation だと思うんだけど、Rails 的には クラスベース OOP の用語をそのまま拝借して Model(クラス)の association と呼ぶらしい。
DBMSの制約はただの約束
- 外部キー制約 = リレーションシップではない
- DBMS はこの制約を無視して構わない
- リレーションシップはテーブル設計を開発者が「そう決めた」から
リレーションシップを設定するのが外部キー制約ではないことに注意してください。外部キー制約は、列の値がターゲットテーブルの既知のキーを参照するかどうかチェックすべきことを、データベースに指示するだけです。DBMS は、この制約を無視して構いません(実際に MySQL の一部のバージョンは無視します)。テーブル間リレーションシップが設定されるのは、開発者が product_id 列と order_id 列に products テーブルと orders テーブルのキー値を入力することを決めたからです。
(『RailsによるアジャイルWebアプリケーション開発』第1版 p232)
「要は外部キーでしょ」と分かった気になっていただけでに少なからずショック。
以下、とりとめもなく感想とか。Before Rails な世界の住人としては最近のアプリの DBMS の schema dump とか見ると制約が全然なくてすごくスカスカした感じがするんだけど、DBMS の制約を使うと変更に弱くなるので使わない、という方針なんだよね。逆に Model の中身は制約というか validation がズラッと並んでいて「おぉこれは」という感じになってる。つまり Model の方に注目しなきゃいけない。だから Model を利用したうえで ER 図っぽく起こしてくれる
Rails ERD – Entity-Relationship Diagrams for Rails
みたいな道具は必須だなと感じた。こういうのなかった時代はどうしてたんだろう、ほんと。素朴な ER 図作成ツールとか使っても実態と合わないわけでしょ?
key と id
- ActiveRecord のデフォルトルールでは primary key は `id' 列(自動でセットされる)
attribute 上では変更は可能。
class Model < ActiveRecord::Base
set_primary_key "code"
end
ってやったら attribute 上は code が primary key になる。ただし migaration の方もいじっておかないと DBMS 上では id のまま。のはず。一応確認したけど間違ってたらごめん。
- 同じく外部キー列の名前は #{table}_id
これも
class Model < ActiveRecord::Base
belongs_to :foo, :foreign_key => 'foreign_key'
end
のように association の option で変更できる。
基本的なAssociation
- belongs_to
- has_one
- has_many
基本的には Rails ガイドを読めば分かる。なめちゃいけない、すごく丁寧。
- Ruby on Rails Guides: A Guide to Active Record Associations
- ruby/rails/RailsGuidesをゆっくり和訳してみたよ/Active Record Associations - 株式会社ウサギィwiki
中でも belongs_to を忘れると話にならないのでとりあえずこれだけ。
class Model < ActiveRecord::Base
belongs_to :table
end
そして自分の所属する table の id ( primary_key ) を保持するカラムを作る migration を用意する。
class AddForeignkeyToModel < ActiveRecord::Migration
def self.up
add_column( :models, :table_id )
end
def self.down
remove_column( :models, :table_id )
end
end
こんな感じ。
この部分は自然な読みやすさを重視してるらしく、has_many だと
class Model < ActiveRecord::Base
has_many :tables
end
になったりする。自然かもしれないけど、決まってないと不安な気もしないではない。
オプションもいっぱいある。
Module: ActiveRecord::Associations::ClassMethods
先に挙げた foreign key の他にもいろいろ。
応用Association
- has_and_belongs_to_many
- has_many , :through =>
- has_one, :through =>
- belongs_to , :polymorphic => true
has_and_belongs_to_many
Module: ActiveRecord::Associations::ClassMethods
いわゆる 多:多, M:N と呼ばれる関連を表現する。has_and_belongs_to を相互に書くことで結合テーブルを隠蔽した多:多の association を定義できる。
※ 素朴な話をすると 多:多 っていう relation は存在しなくて、実際この association も隠蔽されているだけで結合テーブルを利用する。DBMS の基本が分かっているなら 多:多 も特別怖がる必要はない。あくまで ORM は DBMS の使い勝手をよくするもので DBMS の機能に変化はない。
具体的には
Foo < Model
has_and_belongs_to_many :bars
end
Bar < Model
has_and_belongs_to_many :foos
end
と定義すると
foos_bars
という結合テーブルを使って相互に更新される。migration は以下の通り。
class FoosBars < ActiveRecord::Migration
def self.up
create_table :foos do |t|
...
end
create_table :bars do |t|
...
end
create_table :foos_bars, :id => false do |t|
t.column :foo_id, :integer, :null => false
t.column :bar_id, :integer, :null => false
end
end
def self.down
drop_table :foos
drop_table :bars
drop_table :foos_bars
end
end
primary key を持たない(should not have)結合テーブル を手動で migration して作らなければならない(you must manually generate)。
この場合、結合テーブルに該当するモデルは定義する必要がない。言い換えると結合テーブルを操作することはできない。
class Foo
bars.push_with_attributes( barのレコードオブジェクト, column symbol => 値 )
end
という形で参照先のテーブルを更新する。
has_(one|many), :through =>
結合テーブルを利用した association に独自の何かが欲しい場合もある。先ほど隠蔽した 多:多 の association を表現する Model を定義し直すと以下のようになる。
class Foo < Model
has_many :bar, :through => :foo_bar
end
class Bar < Model
has_many :foo, :through => :foo_bar
end
FooBar < Model
belongs_to :foo (, :dependent => :destroy )
belongs_to :bar (, :dependent => :destroy )
end
明示的な Model を作ると独自の処理を自然に記述しやすくなるし、明確な意味を持たせやすい。
上の例では結合テーブルのような名前になっているが、当然名前も自由に決められる。このテーブルの意味が明確になる名前に設定するとよい。
Polymorphic
Model の関係を固定せず、似た association を複数実現する機能。仮想のテーブルをここでは :polymorphable とした場合に具体的に Model 上の記述では
Foo < Model
belongs_to :POLYMORPHABLE, :polymorphic => true
end
Bar < Model
has_XXX :foo, :as => :POLYMORPHABLE
end
の組み合わせで実現する。
この際、migration では
create_table :foos do |t|
t.references, :POLYMORPHABLE, :polymorphic => true
end
と定義する。実際にはこれは
create_table :foos do |t|
t.string, :POLYMORPHABLE_type
t.integer, :POLYMORPHABLE_id
end
という形に展開される。大文字の POLYMORPHABLE はもちろん小文字で。
※ この部分には名詞ではなく形容詞の -able にするのがパターンとして多いみたいだけど、明確な理由は分からない。恐らく複数形への変化の影響を受けないとか :as => -able が自然に読めるとかそういうことだと思う。
特徴は
- belongs_to では物理的なテーブル名ではなく、仮想のテーブル名を指定し、:polymorphic => true を付加
- has 側では物理的なテーブル名を指定するが :as を使って仮想のテーブル名も指定する
- belongs_to 側の table のカラムには has な table 名は直接入らない
- (ここまでに登場した association では bar_id というカラムがあるはず。)
- belongs_to 側の table は has 側の Model 名を #{POLYMORPHABLE}_type に格納する
- ここで Model 名が可変になっているので association を柔軟に変更できる
よく例に挙がるのは画像を格納するテーブルを Polymorphic で紐づける方法。例えば RSS 2.0 などでは blog そのもの、entry それぞれなどに画像を指定できるし、その数も決まっていない。したがって
class Blog < Model
has_one :image, :as => :imagable
end
class Entry < Model
has_many :images, :as => :imagable
belongs_to :blog
end
class Image < Model
belongs_to :imagable, :polymorphic => true
end
のように関係を記述できる。この場合、
- :imagable_type に 'Blog' か 'Entry' が入る
- :imagable_id にそれぞれの Model の id が収まる
ことで複数の Model に belongs_to することができる。若干分かりにくいがとても便利。
上の例の Entry のインスタンスで .images を呼ぶと :image テーブルから自身に紐づいているレコードを取得できる。以下のような感じ。
entry = Entry.find(params[:id])
entry.images
単一テーブル継承との組み合わせに注意
あまりこんなことはやらないかもしれないけど。
- Polymorphic で使われる Model クラス名は DBMS のテーブルに紐づくクラス名
- 単一テーブル継承の子どものクラス名を格納してしまうと検索できない
ので注意。