Ruby on Rails で、ActiveRecord を使ってツリー関係の関連を定義する方法のメモです。

ツリー関係を表す関連にはひとつのテーブルで表す方法と関連テーブルを使って表す方法とがありますが、今回は関連テーブルを使ってツリー関係を表す方法のメモです。

環境は Rails3.0.1、ActiveRecord3.0.1 で確認しています。

ActiveRecord でツリー関係を表す方法

下のようなツリー関係のあるモデルを、belongs_tohas_onehas_many を使って表す方法のメモです。

スクリーンショット(2010-11-20 0.58.43)2.png

モデルの説明

このモデルは、カテゴリ(Category)とカテゴリ関係(CategoryRel)の二つのテーブルを使ってカテゴリのツリー関係を表したものです。

カテゴリ(Category)は親カテゴリを has_one で、サブカテゴリを has_many で保持します。

カテゴリ関連(CategoryRel)は belongs_to で親カテゴリとサブカテゴリをそれぞれ定義しています。

Category モデル

class Category < ActiveRecord::Base
 
  # 親カテゴリ
  has_one :parent_rels, :class_name => "CategoryRel", :foreign_key => "sub_category_id"
  has_one :parent_category, :through => :parent_rels, :source => :parent_category
 
  # サブカテゴリ
  has_many :category_rels
  has_many :sub_categories, :through => :category_rels, :source => :sub_category
end

CategoryRel モデル

class CategoryRel < ActiveRecord::Base
  # 親カテゴリ
  belongs_to :parent_category, :class_name => "Category", :foreign_key => "category_id"
  # サブカテゴリ
  belongs_to :sub_category, :class_name => "Category", :foreign_key => "sub_category_id"
end

has_many :through :source

関連をひとつだけもつ場合には has_one を、1対多、多対多 を表す場合には has_many を使います。:through オプションと :source オプションはそれぞれ次のような意味です。

:through

has_many で定義した関連をどういう経路で取得するかを示す。

:source

:through で示された経路の先で、どの関連を使用するかを示す。

親カテゴリを表す has_one と外部キーの指定の仕方

今回の例で見ていくと、カテゴリは親カテゴリを 0 or 1 持ちます。なので、has_one を指定しています。

親カテゴリの定義では、最初に直接の関連であるカテゴリ関連の定義をしています。それが、has_one :parent_rels, :class_name => "CategoryRel", :foreign_key => "sub_category_id" の部分です。

この関連は、:class_name で指定したクラスの関連、すなわちカテゴリ関連になります。カテゴリ関連との関係を :foreign_key で指定しています。親カテゴリは、カテゴリ関連の sub_category_id が自分のIDと一致するレコードのカテゴリのことになるので、:foreign_key には自分のIDと対応するカラムである sub_category_id を指定しています。

関連のショートカットを定義する

次に、毎回 parent_rels 関連をたどって親カテゴリオブジェクトを取得するのは面倒くさいので、:through を使ってショートカットを定義しています。それが has_one :parent_category, :through => :parent_rels, :source => :parent_category の部分です。

:through にはどの経路を使ってオブジェクトを得るかを指定します。:source には :through でたどった先に関連が二つ以上ある場合にどれを使うかを指定します。カテゴリ関連には親カテゴリとサブカテゴリを表す関連があるので、親カテゴリの関連である :parent_category を指定しています。

:source の考え方
最初、:source は関連の元(ソース)はどっちか?を表すオプションだと思っていたので、自分のインスタンスから見て自分を表すのはどれか?を指定していましたが、これは間違いです。

:source オプションは自分がどっちを表すものではなく、:through で表される経路の取得先を示すものです。なので、自分から見て親カテゴリを表す関連はどっちか?を指定します。

サブカテゴリを表す has_many

サブカテゴリも、基本的には親カテゴリと同じです。サブカテゴリは複数取り得るので has_many を指定しています。

最初に直接の関連であるカテゴリ関連を has_many で定義します。定義名のみを書いておくと、ActiveRecord がモデル名の規約から適切に関連を定義してくれます。

今回は、カテゴリ関連の複数形である :category_rels を has_many で定義しているので、ActiveRecord が自動的に CateogryRel モデルの関連を張ってくれます。

次に、has_many :sub_categories, :through => :category_rels, :source => :sub_category の定義で category_rels の関連を経由して、カテゴリ関連の :sub_category で定義されている関連をサブカテゴリとして定義しています。

まとめ

説明がわかりずらく、逆にわからなくなったかもしれませんが、要は、has_many :through でたどった先に複数の関連がある場合(厳密には規約から推測できない関連があった場合)、:source を使ってどの関連をソースにしてオブジェクトを集めるかを指定する必要があるということです。