目次
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
よくある検索条件をメソッド化できる。
User < ActiveRecord::Base scope :active, where(state: "active") scope :teenager, where("age >= 13 and age <= 19") scope :male, where(gender: 1) scope :age, -> x { where(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.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.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_loadとjoin
eager loading(多対一リレーションなデータで、多を検索したときに一のデータもjoinして取っておく事)する
class Book < ActiveRecord::Base belongs_to author end # 取得されたbookの数だけauthorを取得するSQLを実行(N+1クエリ問題) books = Book .where("title LIKE ?", "あ%") books.each { |book| puts book.author.name }
SELECT .. FROM books WHERE (title LIKE 'あ%'); SELECT .. FROM authors WHERE id = 1; -- booksの取得件数だけSQLがauthorを取得するSQLが実行されてしまう SELECT .. FROM authors WHERE id = 2; SELECT .. FROM authors WHERE id = 5; . . .
# authorも同時に取得(left outer joinを使用) books = Book .where("books,title LIKE ?", "あ%") .eager_load(:author) books.each { |book| puts book.author.name }
SELECT books.*, authors.* FROM books LEFT OUTER JOIN authors ON books.author_id = authors.id WHERE (books.title LIKE 'あ%');
left outer joinのため、 author が nil になる可能性があるので注意。inner joinで結合したい場合はjoinsを併用する
# authorも同時に取得(inner joinを使用) books = Book .where("books.title LIKE ?", "あ%") .eager_load(:author) .joins(:author) books.each { |book| puts book.author.name }
SELECT books.*, authors.* FROM books INNER JOIN authors ON books.author_id = authors.id WHERE (books.title LIKE 'あ%')
joinsを単体で使用するとSQL発行時にauthorsテーブルをinner joinするがauthorデータの取得は行わない。検索条件にauthorを使いたいがauthorのデータは不要な場合に使用する
# 著者名が「あ」で始まる書籍を取得 books = Book .where("author.name LIKE ?", "あ%") .joins(:author)
SELECT books.* FROM books INNER JOIN authors ON books.author_id = authors.id WHERE (author.name LIKE 'あ%')
preload
preloadはjoinせず、2つのSQLを発行して関連データを取得する。
# 著者名が「あ」で始まる書籍を取得 books = Book .where("books.title LIKE ?", "あ%") .preload(:author) books.each { |book| puts book.author.name }
SELECT books.* FROM books WHERE (books,title LIKE 'あ%'); SELECT authors.* FROM authors WHERE authors.id IN (1,2,5,...); -- 最初のSQLで取得されたレコードのauthor_idで検索
includes
includesは特殊で、通常はpreloadと同じ動きだが、結合テーブル側を検索条件に含めるとeager_loadに変わる
books = Book .where("books,title LIKE ?", "あ%") .includes(:author) books.each { |book| puts book.author.name }
SELECT books.* FROM books WHERE (books,title LIKE 'あ%'); SELECT authors.* FROM authors WHERE authors.id IN (1,2,5,...);
author側に条件があるとjoinする(ただしオブジェクト形式での条件指定しかできない)
books = Book .where("books,title LIKE ?", "あ%") .includes(:author) .where(author: {birth_year: 1987}) books.each { |book| puts book.author.name }
SELECT books.*, authors.* FROM books LEFT OUTER JOIN authors ON books.author_id = authors.id WHERE books.title LIKE 'あ%' AND authors.birth_year = 1987;