「多対多の関連」とは、例えばデータにタグを付ける機能などです。データは複数のタグを参照し、タグもまた複数のデータを参照できるような「関連」のことをいいます。このような「関連」を作るには、データとタグのような2つの対象のモデルとは別に、それらの接続情報を持つモデルが必要になります。
ここでは実際に「book_library」のBookモデルにタグ機能を追加して、「多対多の関連」の実装を行ってみましょう。
まずは、タグのモデル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
「多対多の関連」では、対象の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.