DataMapper を使う (Associations)
目次
- has n and belongs_to (One-To-Many)
- has n, :through (One-To-Many-Through)
- ここでちょっと補足
- has, and belongs to, many (Or Many-To-Many)
- Self-Referential Has, and belongs to, many
- Adding To Associations
- Removing From Associations
- Customizing Associations
- Adding Conditions to Associations
- Finders off Associations
- 参照情報
はじめに
ここでいう Association とは,テーブル (DataMapper の場合はモデル) 間の関連性を示します.例えば,Blog の記事 (Post モデル) は複数のコメント (Comment モデル) を持つことから,Posts <-> Comments 間に 1 対 多 の has 関係があります (逆に眺めると,Comment は Post に所属 (belong to) している).
DataMapper では,以上のようなモデル間の関連性を次のように表すことができます (以下の表は公式サイトより引用.ActiveRecord と対比してあります).
ActiveRecord Terminology DataMapper 0.9 Terminology 1 対 多 has_many has n 1 対 1 has_one has 1 所属先指定 belongs_to belongs_to 多 対 多 has_and_belongs_to_many has n, :things, :through => Resource 多 対 多 has_many :things, :through => "Model" has n, :things, :through => :models
- ":things" で子テーブル (モデル) を指定
- ":through" で結合テーブル (モデル) を指定
- "Resource" を指定すると,結合テーブルに匿名オブジェクトを使う
has n and belongs_to (One-To-Many)
Post - Comment 間に 1 対 多 の関係を定義する.
コードは以下の通り.
class Post include DataMapper::Resource property :id, Serial property :body, Text has n, :comments # ↑こっちは複数 end class Comment include DataMapper::Resource property :id, Serial property :body, Text belongs_to :post # ↑こっちは単数 end
実際の関連づけは,以下のようにする.
p = Post.get(1) p.comments.build(:body => '...') p.save # または c = p.comments.build(:body => '...') c.save # または c = Comment.create(:body => '...') p.comments << c
has n, :through (One-To-Many-Through)
結合テーブルを明示する場合.Photo と Tag が Tagging を通してリンクする (このコードは事実上 Many-To-Many になっているため,図はそちらを参照してください).
class Photo include DataMapper::Resource property :id, Serial property :name, String has n, :tags, :through => :taggings # :through => Tagging, # :class_name => Tag, (:tags から) # :child_key => [:photo_id], (self が Photo なので) # :remote_name => :tag (:tags から) # → といった感じだろうか has n, :taggings end class Tagging include DataMapper::Resource belongs_to :photo belongs_to :tag end class Tag include DataMapper::Resource property :id, Serial property :name, String has n, :taggings has n, :photos, :through => :taggings # XXX Tagging で良いのでは? # :through => Tagging, # :class_name => Photo, (:photos から) # :child_key => [:tag_id], (self が Tag なので) # :remote_name => :photo (:photos から) # → といった感じだろうか # or more complicated (different name than class) # クラス名とは異なる名前を付ける場合は以下の通り has n, :pictures, :through => :taggings, :class_name => 'Photo', # :pictures から決まらないため :child_key => [:tag_id], # 一応 self が Tag なので指定なしでも動くが,以下のような注意を受ける # 「...You probably also want to specify the :child_key option.」 :remote_name => :photo # :pictures から決まらないため end
関連づけは以下のようにする.
p1 = Photo.create(:name => 'photo1') t1 = Tag.create(:name => 'tag1') Tagging.create(:photo => p1, :tag => t1) p p1.tags # [#<Tag id=1 body="tag1">] # one-to-many の様な build は使えない # p1.tags.build(:body => 'tag1') # → DataMapper::Associations::ImmutableAssociationError が throw される # 加えて,このままだと p1.tags << t1 という操作ができない # 次の「ここでちょっと補足」の対応を入れると,"<<" については可能になる # (対応を入れた状態で build を使うと,「The property 'photo_id' is not a public property.」となる p1.tags << t1
ここでちょっと補足
上に示した Photo - Tag の例では,:tags を使って直接関連づけすることができなくなる.
class Photo ... has n, :tags, :through => :taggings end p = Photo.create(:name => 'photo_a') t = Tag.create(:name => 'tag_a') p.tags << t # DataMapper::Associations::ImmutableAssociationError が throw される.
これはバグなんだそうで,以下のように :mutable => true を指定すると回避できる.
class Photo ... has n, :tags, :through => :taggings, :mutable => true ~~~~~~~~~~~~~~~~ end
(※ 次に示す Many-To-Many の has n, :through では "<<" が使えることから,:through にモデルを明示すると発現するバグのようです)
has, and belongs to, many (Or Many-To-Many)
class Article include DataMapper::Resource property :id, Serial property :name, String has n, :categories, :through => Resource end class Category include DataMapper::Resource property :id, Serial property :name, String has n, :articles, :through => Resource end
上記のように :through に Resource を指定すると,結合に匿名テーブルが使用される.
具体的にはこんな感じ.
mysql> show tables; +-----------------------+ | Tables_in_dm_ass_test | +-----------------------+ | articles | | articles_categories | | categories | +-----------------------+ mysql> describe articles_categories; +-------------+---------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+---------+------+-----+---------+-------+ | category_id | int(11) | NO | PRI | NULL | | | article_id | int(11) | NO | PRI | NULL | | +-------------+---------+------+-----+---------+-------+
2 つのテーブル名を連結した名前の結合テーブルが作成される.
関連づけは以下のようにする.
a = Article.create(:name => 'aaa') c1 = Category.create(:name => 'c1') a.categories << c1 a.save # one-to-many の様な build は使えない # a.categories.build(:name => 'c1') # → The property 'article_id' is not a public property.
対象とするモデルが特定の名前空間に属する場合は,:class_name で明示する必要がある.
例えば,
module Blog class Article ... end class Category ... end end
の場合,
class Blog::Article include DataMapper::Resource ... has n, :categories, :through => Resource, :class_name => 'Blog::Category' end class Blog::Category include DataMapper::Resource ... has n, :articles, :through => Resource, :class_name => 'Blog::Article' end
とする.
Self-Referential Has, and belongs to, many
(※ ここはかなりつまずいてしまったため,コメント多めになってます.というか,:remote_name の説明がみつけられなかった...以下,大部分が挙動からの推測です.)
class User include DataMapper::Resource property :id, Serial property :name, String, :nullable => false has n, :friended_users # self.friended_users # ↓ # SELECT * FROM friended_users WHERE friended_users.user_id == self.id has n, :friends, :class_name => 'User', # 扱うクラスは? :through => :friended_users, # 結合テーブルは? :child_key => [:user_id] # child=FriendedUser のどの property と id を一致させるか? # :remote_name は :friends → :friend と決まるため,省略可 # self.friends # ↓ # SELECT users.id, users.name FROM users # ~~~~~ # INNER JOIN friended_users ON (users.id == friended_users.friend_id) # ~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # WHERE friended_users.user_id == self.id # ~~~~~~~ # self から見た友達 (User) を返す. has n, :friended_by, :class_name => 'User', # 扱うクラスは? :through => :friended_users, # 結合テーブルは? :child_key => [:friend_id], # child=FriendedUser のどの property と id を一致させるか? :remote_name => :user # 結合テーブルのレコードから User を引く方法は? # → belongs_to :user を使う # self.friended_by # ↓ # SELECT users.id, users.name FROM users # ~~~~~ # INNER JOIN friended_users ON (users.id == friended_users.user_id) # ~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # WHERE friended_users.friend_id == self.id # ~~~~~~~~~ # 「self は友達」としている User を返す. end class FriendedUser include DataMapper::Resource property :user_id, Integer, :key => true property :friend_id, Integer, :key => true belongs_to :user, :child_key => [:user_id] # User とは :user_id で リンクする # ↑このモデルであることは :user から決まる belongs_to :friend, :child_key => [:friend_id], :class_name => 'User' # User とは :friend_id でリンクする # ↑ User であることは :class_name から決まる.:user の様に名前からは決められない. end
使用例は以下で紹介されている.
Adding To Associations
まとめると以下のようになる.
- has n, belongs_to
- One-To-Many
- build, << で関連を追加
- has n, has n (:through)
- Many-To-Many
- 基本は結合テーブルのモデルで関連を追加
- :mutable => true で << も使用可能
- build は使用不可
Removing From Associations
テーブル間の関連性は,結合テーブルのレコードを削除することで解消する.
結合テーブルに匿名を使っている場合 (:through => Resource),結合テーブルモデルの名前は,結合関係にあるモデルの名前を連結したものになる.
article = Article.first category = article.categories.first # 結合レコードを削除 join = ArticleCategory.first(:article_id => article.id, :category_id => category.id) ~~~~~~~~~~~~~~~ unless join.nil? join.destroy end # 関連が変更されたため,reload が必要 article.categories.reload category.articles.reload
Customizing Associations
DataMapper では,規約に基づいて関連性の定義からクラスや外部キー (子から見た親の主キー) を特定する.
これらの情報を調整する必要がある場合は,has の使用例でも挙げたように,:class_name や :child_key を明示する必要がある.
class Group include DataMapper::Resource ... belongs_to :manager, :class_name => 'Person', :child_key => [:manager_id] belongs_to :coordinator, :class_name => 'Person', :child_key => [:coordinator_id] end
Adding Conditions to Associations
association の定義には,いくつかの条件を付与することができる.
class Post include DataMapper::Resource ... has n, :comments, :order => [:published_on.desc], :rating.gte => 5 # :published_on の降順で,かつ :rating が 5 以上の Comment が得られる end class Comment include DataMapper::Resource ... property :published_on, String property :rating, Integer belongs_to :post end
Finders off Associations
モデルから Association (has n, :hoges の 'hoges') を呼び出したとき,DataMapper は内部でクエリオブジェクトを作る.
それはイテレーションを開始したときや #length を呼び出したときに実行される.
(中身を使い始めるまではクエリを発行しない,という DataMapper の良いところ)
Association から all や first を呼び出したり,通常の all/first とまったく同じ引数を与えた場合 (←そういう呼び出し方ができる),DataMapper は Association のクエリに新しいクエリ (前出の引数) をマージする.
前述の通り,何らかのメソッド呼び出しがあると,Association クエリの結果から新しいクエリで要求された分のサブセットを返す.
post = Post.first p post.categories # この post と関連する全ての category を返す p post.categories.all(:limit => 10, :order => [:name.asc]) # 最初の 10 件を名前の昇順で返す p post.categories(:limit => 10, :order => [:name.asc]) # all 呼び出しの alias,all 呼び出しと同じ結果になる
参照情報
- DataMapper
- 0.9.11
- ドキュメント