連載
» 2014年06月30日 18時00分 公開

ActiveRecordにおけるモデルの「関連」とコールバックの使い方開発現場でちゃんと使えるRails 4入門(6)(2/3 ページ)

[著:林慶、監修:山根剛司,株式会社アジャイルウェア]

接続情報を持つモデルも必要な「多対多の関連」

 「多対多の関連」とは、例えばデータにタグを付ける機能などです。データは複数のタグを参照し、タグもまた複数のデータを参照できるような「関連」のことをいいます。このような「関連」を作るには、データとタグのような2つの対象のモデルとは別に、それらの接続情報を持つモデルが必要になります。

 ここでは実際に「book_library」のBookモデルにタグ機能を追加して、「多対多の関連」の実装を行ってみましょう。

Taggingモデルの作成

 まずは、タグのモデルTagと、BookモデルとTagモデルの接続情報を保存するTaggingモデルを「rails g(enerator)」コマンドで作ります。

% rails g model tag name:string
% rails g model tagging tag:references book:references
% rake db:migrate

 コマンド中の「references」は参照型です。「rails g model tagging tag_id:integer book_id:integer」でも同じ機能を実装できますが、「references」型ではデータベースにインデックスが追加され、モデルの定義にあらかじめ「belongs_to」を含めてくれます。

 ただし、「references」型が使えるのはテーブル生成時だけです。従って、生成された「Tag」と「Tagging」のモデルのコードは次のようになっています。

class Tag < ActiveRecord::Base
end
class Tagging < ActiveRecord::Base
  belongs_to :tag
  belongs_to :book
end

「一対多の関連」のhas_manyを使う

 「多対多の関連」では、対象の2モデルの両方が、中間モデルに対して「一対多の関連」を持ちます。そのため、TagモデルとBookモデルに次の行を追加してください。

has_many :taggings

 そして、「多対多の関連」を定義するにはTagモデルとBookモデルに次の一文を加えます。

# Tagモデル
has_many :books, through: :taggings
# Bookモデル
has_many :tags, through: :taggings

実行して確認

 これで、「多対多の関連」が実装されました。「rails console」で実際に使ってみましょう。

tag_ruby = Tag.create(name: 'Ruby')
book_ruby = Book.create(title: 'Ruby入門')
tag_ruby.books << book_ruby
tag_ruby.books.create(title: 'Rails入門')
tag_ruby.books
#=> #<ActiveRecord::Associations::CollectionProxy [
      #<Book id: 1, title: "Ruby入門", ...>,
      #<Book id: 2, title: "Rails入門", ...>
    ]>
# 「多対多の関連」の機能は対称
book_rails = Book.last
book_rails.tags << Tag.create(name: 'Rails')
book_rails.tags
#=> #<ActiveRecord::Associations::CollectionProxy [
      #<Tag id: 1, name: "Ruby", ...>,
      #<Tag id: 2, name: "Rails", ...>
    ]>
# またTagging.allを取得すると次のようになっています。
#=> #<ActiveRecord::Relation [
      #<Tagging id: 1, tag_id: 1, book_id: 1, ...>,
      #<Tagging id: 2, tag_id: 1, book_id: 2, ...>,
      #<Tagging id: 3, tag_id: 2, book_id: 2, ...>
    ]>
# tag_id: 1が"Ruby"のタグ, tag_id: 2が"Rails"のタグ
# book_id: 1が"Ruby入門"の本, book_id: 2が"Rails入門"の本
# "Ruby入門"には"Rails"タグは付いていないのでTaggingのモデルは3個となります。

 このように、「多対多の関連」は中間モデルとhas_manyの:throughオプションで簡単に実装できます。

「関連」に基づく検索

 「関連」している複数のオブジェクトについては、検索を行うことも可能です。例えば、上のTagモデルとBookモデルの「関連」では次のようなことができます。

book_rails = Book.find_by(title: 'Rails入門')
book_rails.tags.where(name: 'Ruby')
#=> #<ActiveRecord::AssociationRelation [
      #<Tag id: 1, name: "Ruby", ...>
    ]>

 このような使い方は「多対多の関連」に限らず、「一対多の関連」でも可能です。

 また、Bookの関連先のモデルの属性を対象に検索することもできます。そのためには「joins」メソッドを使います。

 「joins」の引数には「関連」名をとりますが、続く「where」メソッドの引数のHashのキーの「tags」はテーブル名になります。

Book.joins(:tags).where(:tags: {name: 'Rails入門'})
#=> #<ActiveRecord::Relation [
      #<Book id: 2, title: "Rails入門", ...>
    ]>

 さらに、whereには以下のように文字列をSQLライクに渡すこともできます。

Book.joins(:tags).where("tags.name = ?", 'Rails入門')
#=> #<ActiveRecord::Relation [
      #<Book id: 2, title: "Rails入門", ...>
    ]>

 この場合、「?」の部分に'Rails'という文字列が渡されてSQLが組み立てられます。

「スコープ」による検索条件の登録

 検索のメソッドの組み立てを何度もコントローラーですることは保守性を下げます。そこで、よく使う検索条件にはモデル中にあらかじめ名前を付けて定義しておく「スコープ」という仕組みが用意されています。

 以下のように第1引数にスコープ名、第2引数に検索条件をラムダ(->)で与えることで定義ができます。

class Book < ActiveRecord::Base
  has_many :taggings, foreign_key: 'tag_id'
  has_many :tags, through: :taggings
  scope :tagged_recommended, -> { joins(:tags).where(tags: {name: 'recommended'}) }
end

 ラムダで渡すのは処理の中身を遅延実行させるためで、例えば処理の中でDate.nowなど現在時刻を取得する処理をしている場合などに予期せぬ動作を防ぎます。このため、処理の中身をラムダで渡すのは、Rails 4から必須となりました。これにより、Bookモデルは'recommended'のタグが付いたレコードを以下のように検索できます。

Book.tagged_recommended
#=> #<ActiveRecord::Relation [...

 また、デフォルトスコープの設定もできます。モデル定義内で次のように「default_scope」を宣言することで定義できます。

default_scope { where(...) }

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。