文書の過去の版を表示しています。
目次
ActiveRecord
RailsのO/Rマッパ&モデルオブジェクト。Railsから独立して使う事も可能。
テーブル規約
調べ中というか記憶の中から引っ張り出しているので間違ってるかも
テーブル名
- 単語の複数形(hoges)
- hogesテーブルに対応するActiveRecordクラスはHoge
- アンダースコア入りの場合 piyopiyo_hoges → PiyopiyoHoge
- 多対多の中間テーブルは「テーブル1の複数形_テーブル2複数形」(hoges_fugas)
カラム名
- 主キー:id
- 外部キー:外部テーブル名単数形_外部テーブルの主キー名(例:fuga_id)
- アンダースコアで連結したカラム名に対応するプロパティ名: そのまま hoge_date → hoge_date
多対多
ActiveRecordで多対多を実装する方法は2つある。
- has_and_belongs_to_many (habtm) 2つのテーブルのidのみをもつリレーション専用の中間テーブルを使う方法、モデルクラスを持たない
- has_many :through リレーションに2つのテーブルのidをもつActiveRecordモデルクラスを使う
require "rubygems" require "active_record" ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => "test.db") ActiveRecord::Schema.define do create_table :bookmarks do |t| t.column :name, :string t.column :url, :string t.column :memo, :string end create_table :tags do |t| t.colmn :name, :string end create_table :taggings do |t| t.column :bookmark_id, :integer t.column :tag_id, :integer end end # bookmarksテーブルとtagsテーブルは多対多(中間テーブルtaggings) class Bookmark < ActiveRecord::Base has_many :bookmark_tags: has_many :tags, :through => :taggings end class Tag < ActiveRecord::Base has_many :bookmark_tags: has_many :bookmarks, :through => :taggings end class Tagging < ActiveRecord::Base belongs_to :bookmark belongs_to :tag end
Observer
after_saveなどのイベント駆動なメソッドを外だししたもの。実装がObserverパターンかどうかは知らん。
hogeモデルのオブサーバクラスを作成するには…
$ script/generate observer hoge
nemed_scope
(rails 2.1 or later)
よくある検索条件をメソッド化できる。
User < ActiveRecord::Base named_scope :active, :conditions => { "state" => "active" } named_scope :teenager, :conditions => {"age" => 13..19 } named_scope :male :condtions => {"sex" => 1 } named_scope :age, lambda{ |x| { :conditions => ["age = ?", x] }} end
小さい条件をたくさん作って、メソッドチェインで組み合わせるのがポイント
# 15歳男性の有効なユーザを取得 active_fifteen_boys = User.active.age(15).male
SQLは遅延実行されるので、複数のnamed_scopeでチェインしてもSQLが実行されるのは一度だけ。
ポリモーフィック・アソシエーション
複数のモデルからhas_many、has_oneされるモデルを作る事ができる。
所有されるモデル
class Attachment < ActiveRecord::Base belongs_to :attachable, :polymorphic = true end
所有するモデル
class Document < ActiveRecord::Base has_many :attachments, :as => attachable end class Mail < ActiveRecord::Base has_many :attachments, :as => attachable end
belongs_toの対象が複数のモデルではないので、テーブルを直指定できない。そこでattachableという仮のモデル名を付け、DocumentとMailがattachableなモデルであるとして扱う、というイメージ
belongs_toの側にはattachable_idとattachable_type(typeのみでも良いようだ)というカラムが必要。
単一テーブル継承
1つのテーブルから振る舞いの異なる複数のActiveRecordクラスを作りたいときに使う。テーブルにtypeというStringのカラムを作成しておく。
例:items(商品)テーブル
id | integer | ID |
type | string | 型 |
name | string | 商品名 |
sold_from | date | 販売開始日 |
sold_from | date | 販売終了日 |
で、通常商品(常に販売される)、限定商品(販売期間が限定される)で振る舞いを変えたい場合
class Item end class RegularItem < Item # 通常商品 # sold_fromやsold_toの値に関わらず常にtrue def on_sale? true end end class LimitedItem < Item # 限定商品 # 今日の日付がsold_fromとsold_toのときのみ販売中 def on_sale? today = Date.today sold_from <= today && sold_to >= today end end
このように同じテーブルを利用しながらも、振る舞いの異なるクラスを作成できる。注意点として、ItemとLimitedItemとRegularItemは別ファイルに作成する。models/item.rbにLimitedItemとRegularItemのクラスを書いても動作してくれない。
検索時は
LimitedItem.find(:all)
とすれば、
select * from items where type = "LimitedItem";
というSQLが生成される。typeは自動的にセットされる。また、insertでは、
RegularItem.create(:name => "オプーナ購入権")
とすれば
insert into items (type, name) values ("RegularItem", "オプーナ購入権")
子クラスでデータにアクセスすれば、typeカラムを意識する必要はない。親クラスのItemでinsertやupdateも可能だがtypeをプログラマが管理する必要があるのでちょっと面倒。とくに画面を作る時に死ぬほど面倒なのでLimitedItemとRegularItemそれぞれに別にControllerとViewを作成しよう。
index画面については同一画面上に複数クラスを並べるのも容易なので、ItemControllerにindexメソッドを作成することもできる。
class ItemsController def index @items = Item.find(:all) end end
としたとき、index.html.erbで
@items.each |item| link_to "Show", item link_to "Edit", edit_item_url(item) end
というscaffoldが作成するコードでは、edit_item_urlが要求する型と実際の型が違うためエラーになってしまう、こういうときはpolymorphic_urlという便利メソッドを使おう。
@items.each |item| link_to "Show", item link_to "Edit", edit_polymorphic_url(item) end
edit_polymorphic_urlは実際の型を見て、RegularItemsController#editかLimitedItemsController#editに割り振る。
カウンタキャッシュ
一対多関連で、一側のオブジェクトから関連する多のレコードがいくつあるか調べるには、
hoge.fugas.count hoge.fugas.size
とすれば良いが、毎回countのSQLが実行されるので場合によっては非効率。
railsには多テーブルを操作したときに、一のテーブルに自動的に関連する多のレコードの数を更新するcounter_cacheという機能がある。
class Hoge has_many :fugas end class Fuga belongs_to :hoge, :counter_cache => true end
とモデルを定義して、Hogeのテーブルには fugas_count というカラムを作っておくと fuga テーブルを操作したときに自動的に関連する hoge テーブルの fugas_count カラムを修正するようになる。
カウンタキャッシュは実際にレコード数をcountしてセットしているのではなく、create時に+1、delete時に-1している。よって初期値が正しくセットしなければならない。カウンタキャッシュのデフォルト値は 0 に設定する。あとからカウンタキャッシュを追加する場合にはmigrationで現在のレコード数をカウンタキャッシュに設定する。
add_column :hoge, :fugas_count, :default => 0 execute "update hoge set fugas_count = (select count(*) from fuga where fuga.hoge_id = hoge.id);"
当然、railsを通さずに直接レコードを追加、削除するとカウンタキャッシュの値が不正になるので注意。
eager loading で inner join
Rails 2.3.4
eager loading(多対一リレーションなデータで、多を検索したときに一のデータもjoinして取っておく事)するには include オプションを使う。
class Book < ActiveRecord::Base belongs_to author end # left outer join books = Book.find( :conditions => ["title LIKE ?", "あ%"], :include => [author]) books.each { |book| puts book.author.name }
関連テーブルの内容はleft outer joinで取得される。よって author が nil にもなるので注意。
inner joinでeager loadingしたいときは joins と include を両方指定する。
books = Book.find( :conditions => ["title LIKE ?", "あ%"], :include => [:author], :joins => [:author]) books.each { |book| puts book.author.name }
joinsだけ指定すると関連テーブルを inner join するが、関連テーブルの内容は eager loading されない。joinsは関連オブジェクトが存在するレコードだけを取得するためのオプション、includeは関連オブジェクトをeager loadingのためのオプションであって役割が別なのである。
joinsとincludeオプションを両方指定したとき、古いwill_paginateプラグインを使っていると、SQLでレコード数をカウントする時に left outer join と inner join 両方含まれてしまう(結果はinner joinのみの場合と同じ)。これはwill_paginate 2.3で修正されている。