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 呼び出しと同じ結果になる