ユーザ用ツール

サイト用ツール


rails:active_record

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つある。

  1. has_and_belongs_to_many (habtm) 2つのテーブルのidのみをもつリレーション専用の中間テーブルを使う方法、モデルクラスを持たない
  2. 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(商品)テーブル

idintegerID
typestring
namestring商品名
sold_fromdate販売開始日
sold_fromdate販売終了日

で、通常商品(常に販売される)、限定商品(販売期間が限定される)で振る舞いを変えたい場合

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;
rails/active_record.txt · 最終更新: 2024/01/10 14:30 by nullpon