2011-02-24

今さら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 ガイドを読めば分かる。なめちゃいけない、すごく丁寧。

中でも 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 のテーブルに紐づくクラス名
  • 単一テーブル継承の子どものクラス名を格納してしまうと検索できない

ので注意。

参考

About

例によって個人のなんちゃらです

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Document SVG AsciiDoc Pandoc DocBook Develop Jekyll macOS Node.js Vite Heroku Transformer AI Data Cloud Wasm