Associations

Associations are a way of declaring relationships between models, for example a blog Post “has many” Comments, or a Post belongs to an Author. They add a series of methods to your models which allow you to create relationships and retrieve related models along with a few other useful features. Which records are related to which are determined by their foreign keys.

The types of associations currently in DataMapper are:

DataMapper Terminology ActiveRecord Terminology
has n has_many
has 1 has_one
belongs_to belongs_to
has n, :things, :through => Resource has_and_belongs_to_many
has n, :things, :through => :model has_many :association, :through => Model

Declaring Associations

This is done via declarations inside your model class. The class name of the related model is determined by the symbol you pass in. For illustration, we’ll add an association of each type. Pay attention to the pluralization or the related model’s name.

has n and belongs_to (or One-To-Many)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Post
  include DataMapper::Resource

  property :id, Serial

  has n, :comments
end

class Comment
  include DataMapper::Resource

  property :id,     Serial
  property :rating, Integer

  belongs_to :post  # defaults to :required => true

  def self.popular
    all(:rating.gt => 3)
  end
end

The belongs_to method accepts a few options. As we already saw in the example above, belongs_to relationships will be required by default (the parent resource must exist in order for the child to be valid). You can make the parent resource optional by passing :required => false as an option to belongs_to.

If the relationship makes up (part of) the key of a model, you can tell DM to include it as part of the primary key by adding the :key => true option.

has n, :through (or One-To-Many-Through)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Photo
  include DataMapper::Resource

  property :id, Serial

  has n, :taggings
  has n, :tags, :through => :taggings
end

class Tag
  include DataMapper::Resource

  property :id, Serial

  has n, :taggings
  has n, :photos, :through => :taggings
end

class Tagging
  include DataMapper::Resource

  belongs_to :tag,   :key => true
  belongs_to :photo, :key => true
end

Note that some options that you might wish to add to an association have to be added to a property instead. For instance, if you wanted your association to be part of a unique index rather than the key, you might do something like this.

1
2
3
4
5
6
7
8
9
10
11
class Tagging
  include DataMapper::Resource

  property :id, Serial
  
  property :tag_id,          :unique_index => :uniqueness, :required => true
  property :tagged_photo_id, :unique_index => :uniqueness, :required => true

  belongs_to :tag
  belongs_to :tagged_photo, 'Photo'
end

Has, and belongs to, many (Or Many-To-Many)

The use of Resource in place of a class name tells DataMapper to use an anonymous resource to link the two models up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# When auto_migrate! is being called, the following model
# definitions will create an
#
#  ArticleCategory
#
# model that will be automigrated and that will act as the join
# model. DataMapper just picks both model names, sorts them
# alphabetically and then joins them together. The resulting
# storage name follows the same conventions it would if the
# model had been declared traditionally.
#
# The resulting model is no different from any traditionally
# declared model. It contains two belongs_to relationships
# pointing to both Article and Category, and both underlying
# child key properties form the composite primary key (CPK)
# of that model. DataMapper uses consistent naming conventions
# to infer the names of the child key properties. Since it's
# told to link together an Article and a Category model, it'll
# establish the following relationships in the join model.
#
#  ArticleCategory.belongs_to :article,  'Article',  :key => true
#  ArticleCategory.belongs_to :category, 'Category', :key => true
#
# Since every many to many relationship needs a one to many
# relationship to "go through", these also get set up for us.
#
#  Article.has n, :article_categories
#  Category.has n, article_categories
#
# Essentially, you can think of ":through => Resource" being
# replaced with ":through => :article_categories" when DM
# processes the relationship definition.
#
# This also means that you can access the join model just like
# any other DataMapper model since there's really no difference
# at all. All you need to know is the inferred name, then you can
# treat it just like any other DataMapper model.

class Article
  include DataMapper::Resource

  property :id, Serial

  has n, :categories, :through => Resource
end

class Category
  include DataMapper::Resource

  property :id, Serial

  has n, :articles, :through => Resource
end

# create two resources
article  = Article.create
category = Category.create

# link them by adding to the relationship
article.categories << category
article.save

# link them by creating the join resource directly
ArticleCategory.create(:article => article, :category => category)

# unlink them by destroying the related join resource
link = article.article_categories.first(:category => category)
link.destroy

# unlink them by destroying the join resource directly
link = ArticleCategory.get(article.id, category.id)
link.destroy

Self referential many to many relationships

Sometimes you need to establish self referential relationships where both sides of the relationship are of the same model. The canonical example seems to be the declaration of a Friendship relationship between two people. Here’s how you would do that with DataMapper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
  include DataMapper::Resource

  property :id,    Serial
  property :name , String, :required => true

  has n, :friendships, :child_key => [ :source_id ]
  has n, :friends, self, :through => :friendships, :via => :target
end

class Friendship
  include DataMapper::Resource

  belongs_to :source, 'Person', :key => true
  belongs_to :target, 'Person', :key => true
end

The Person and Friendship model definitions look pretty straightforward at a first glance. Every Person has an id and a name, and a Friendship points to two instances of Person.

The interesting part are the relationship definitions in the Person model. Since we’re modelling friendships, we want to be able to get at one person’s friends with one single method call. First, we need to establish a one to many relationship to the Friendship model.

1
2
3
4
5
6
7
8
9
10
11
12
class Person

  # ...

  # Since the foreign key pointing to Person isn't named 'person_id',
  # we need to override it by specifying the :child_key option. If the
  # Person model's key would be something different from 'id', we would
  # also need to specify the :parent_key option.

  has n, :friendships, :child_key => [ :source_id ]

end

This only gets us half the way though. We can now reach associated Friendship instances by traversing person.friendships. However, we want to get at the actual friends, the instances of Person. We already know that we can go through other relationships in order to be able to construct many to many relationships.

So what we need to do is to go through the friendship relationship to get at the actual friends. To achieve that, we have to tweak various options of that many to many relationship definition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person

  # ...

  has n, :friendships, :child_key => [ :source_id ]

  # We name the relationship :friends cause that's the original intention
  #
  # The target model of this relationship will be the Person model as well,
  # so we can just pass self where DataMapper expects the target model
  # You can also use Person or 'Person' in place of self here. If you're
  # constructing the options programmatically, you might even want to pass
  # the target model using the :model option instead of the 3rd parameter.
  #
  # We "go through" the :friendship relationship in order to get at the actual
  # friends. Since we named our relationship :friends, DataMapper assumes
  # that the Friendship model contains a :friend relationship. Since this
  # is not the case in our example, because we've named the relationship
  # pointing to the actual friend person :target, we have to tell DataMapper
  # to use that relationship instead, when looking for the relationship to
  # piggy back on. We do so by passing the :via option with our :target

  has n, :friends, self, :through => :friendships, :via => :target

end

Another example of a self referential relationship would be the representation of a relationship where people can follow other people. In this situation, any person can follow any number of other people.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class Person

  class Link

    include DataMapper::Resource

    storage_names[:default] = 'people_links'

    # the person who is following someone
    belongs_to :follower, 'Person', :key => true

    # the person who is followed by someone
    belongs_to :followed, 'Person', :key => true

  end

  include DataMapper::Resource

  property :id,   Serial
  property :name, String, :required => true


  # If we want to know all the people that John follows, we need to look
  # at every 'Link' where John is a :follower. Knowing these, we know all
  # the people that are :followed by John.
  #
  # If we want to know all the people that follow Jane, we need to look
  # at every 'Link' where Jane is :followed. Knowing these, we know all
  # the people that are a :follower of Jane.
  #
  # This means that we need to establish two different relationships to
  # the 'Link' model. One where the person's role is :follower and one
  # where the person's role is to be :followed by someone.

  # In this relationship, the person is the follower
  has n, :links_to_followed_people, 'Person::Link', :child_key => [:follower_id]

  # In this relationship, the person is the one followed by someone
  has n, :links_to_followers, 'Person::Link', :child_key => [:followed_id]


  # We can then use these two relationships to relate any person to
  # either the people followed by the person, or to the people this
  # person follows.

  # Every 'Link' where John is a :follower points to a person that
  # is followed by John.
  has n, :followed_people, self,
    :through => :links_to_followed_people, # The person is a follower
    :via     => :followed

  # Every 'Link' where Jane is :followed points to a person that
  # is one of Jane's followers.
  has n, :followers, self,
    :through => :links_to_followers, # The person is followed by someone
    :via     => :follower

  # Follow one or more other people
  def follow(others)
    followed_people.concat(Array(others))
    save
    self
  end

  # Unfollow one or more other people
  def unfollow(others)
    links_to_followed_people.all(:followed => Array(others)).destroy!
    reload
    self
  end

end

Adding To Associations

Adding resources to many to one or one to one relationships is as simple as assigning them to their respective writer methods. The following example shows how to assign a target resource to both a many to one and a one to one relationship.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person
  include DataMapper::Resource

  has 1, :profile
end

class Profile
  include DataMapper::Resource

  belongs_to :person
end

# Assigning a resource to a one-to-one relationship

person  = Person.create
person.profile = Profile.new
person.save

# Assigning a resource to a many-to-one relationship

profile = Profile.new
profile.person = Person.create
profile.save

Adding resources to any one to many or many to many relationship, can basically be done in two different ways. If you don’t have the resource already, but only have a hash of attributes, you can either call the new or the create method directly on the association, passing it the attributes in form of a hash.

1
2
3
4
5
6
7
8
9
10
11
post = Post.get(1)  # find a post to add a comment to

# This will add a new but not yet saved comment to the collection
comment = post.comments.new(:subject => 'DataMapper ...')

# Both of the following calls will actually save the comment
post.save     # This will save the post along with the newly added comment
comment.save  # This will only save the comment

# This will create a comment, save it, and add it to the collection
comment = post.comments.create(:subject => 'DataMapper ...')

If you already have an existing Comment instance handy, you can just append that to the association using the << method. You still need to manually save the parent resource to persist the comment as part of the related collection.

1
2
3
4
5
post.comments << comment  # append an already existing comment

# Both of the following calls will actually save the comment
post.save           # This will save the post along with the newly added comment
post.comments.save  # This will only save the comments collection

One important thing to know is that for related resources to know that they have changed, you must change them via the API that the relationship (collection) provides. If you cannot do this for whatever reason, you must call reload on the model or collection in order to fetch the latest state from the storage backend.

The following example shows this behavior for a one to many relationship. The same principle applies for all other kinds of relationships though.

1
2
3
4
5
6
7
8
9
10
11
class Person
  include DataMapper::Resource
  property :id, Serial
  has n, :tasks
end

class Task
  include DataMapper::Resource
  property :id, Serial
  belongs_to :person
end

If we add a new task not by means of the API that the tasks collection provides us, we must reload the collection in order to get the correct results.

1
2
3
4
5
6
7
8
9
10
11
12
ree-1.8.7-2010.02 > p = Person.create
 => #<Person @id=1>
ree-1.8.7-2010.02 > t = Task.create :person => p
 => #<Task @id=1 @person_id=1>
ree-1.8.7-2010.02 > p.tasks
 => [#<Task @id=1 @person_id=1>]
ree-1.8.7-2010.02 > u = Task.create :person => p
 => #<Task @id=2 @person_id=1>
ree-1.8.7-2010.02 > p.tasks
 => [#<Task @id=1 @person_id=1>]
ree-1.8.7-2010.02 > p.tasks.reload
 => [#<Task @id=1 @person_id=1>, #<Task @id=2 @person_id=1>]

Customizing Associations

The association declarations make certain assumptions about the names of foreign keys and about which classes are being related. They do so based on some simple conventions.

The following two simple models will explain these default conventions in detail, showing relationship definitions that solely rely on those conventions. Then the same relationship definitions will be presented again, this time using all the available options explicitly. These additional versions of the respective relationship definitions will have the exact same effect as their simpler counterparts. They are only presented to show which options can be used to customize various aspects when defining relationships.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class Blog
  include DataMapper::Resource

  # The rules described below apply equally to definitions
  # of one-to-one relationships. The only difference being
  # that those would obviously only point to a single resource.

  # However, many-to-many relationships don't accept all the
  # options described below. They do support specifying the
  # target model, like we will see below, but they do not support
  # the :parent_key and the :child_key options. Instead, they
  # support another option that's available to many-to-many
  # relationships exclusively. This option is called :via, and
  # will be explained in more detail in its own paragraph below.

  # - This relationship points to multiple resources
  # - The target resources will be instances of the 'Post' model
  # - The local parent_key is assumed to be 'id'
  # - The remote child_key is assumed to be 'blog_id'
  #   - If the child model (Post) doesn't define the 'blog_id'
  #     child key property either explicitly, or implicitly by
  #     defining it using a belongs_to relationship, it will be
  #     established automatically, using the defaults described
  #     here ('blog_id').

  has n, :posts

  # The following relationship definition has the exact same
  # effect as the version above. It's only here to show which
  # options control the default behavior outlined above.

  has n, :posts, 'Post',
    :parent_key => [ :id ],      # local to this model (Blog)
    :child_key  => [ :blog_id ]  # in the remote model (Post)

end

class Post
  include DataMapper::Resource

  # - This relationship points to a single resource
  # - The target resource will be an instance of the 'Blog' model
  # - The locally established child key will be named 'blog_id'
  #   - If a child key property named 'blog_id' is already defined
  #     for this model, then that will be used.
  #   - If no child key property named 'blog_id' is already defined
  #     for this model, then it gets defined automatically.
  # - The remote parent_key is assumed to be 'id'
  #   - The parent key must be (part of) the remote model's key
  # - The child key is required to be present
  #   - A parent resource must exist and be assigned, in order
  #     for this resource to be considered complete / valid

  belongs_to :blog

  # The following relationship definition has the exact same
  # effect as the version above. It's only here to show which
  # options control the default behavior outlined above.
  #
  # When providing customized :parent_key and :child_key options,
  # it is not necessary to specify both :parent_key and :child_key
  # if only one of them differs from the default conventions.
  #
  # The :parent_key and :child_key options both accept arrays
  # of property name symbols. These should be the names of
  # properties being (at least part of) a key in either the
  # remote (:parent_key) or the local (:child_key) model.
  #
  # If the parent resource need not be present in order for this
  # model to be considered complete, :required => false can be
  # passed to stop DataMapper from establishing checks for the
  # presence of the attribute value.

  belongs_to :blog, 'Blog',
    :parent_key => [ :id ],       # in the remote model (Blog)
    :child_key  => [ :blog_id ],  # local to this model (Post)
    :required   => true           # the blog_id must be present

end

In addition to the :parent_key and :child_key options that we just saw, the belongs_to method also accepts the :key option. If a belongs_to relationship is marked with :key => true, it will either form the complete primary key for that model, or it will be part of the primary key. The latter will be the case if other properties or belongs_to definitions have been marked with :key => true too, to form a composite primary key (CPK). Marking a belongs_to relationship or any property with :key => true, automatically makes it :required => true as well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Post
  include DataMapper::Resource

  belongs_to :blog, :key => true  # 'blog_id' is the primary key
end

class Person
  include DataMapper::Resource

  property id, Serial
end

class Authorship
  include DataMapper::Resource

  belongs_to :post,   :key => true  # 'post_id'   is part of the CPK
  belongs_to :person, :key => true  # 'person_id' is part of the CPK
end

When defining many to many relationships you may find that you need to customize the relationship that is used to “go through”. This can be particularly handy when defining self referential many-to-many relationships like we saw above. In order to change the relationship used to “go through”, DataMapper allows us to specifiy the :via option on many to many relationships.

The following example shows a scenario where we don’t use :via for defining self referential many to many relationships. Instead, we will use :via to be able to provide “better” names for use in our domain models.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Post
  include DataMapper::Resource

  property :id, Serial

  has n, :authorships

  # Without the use of :via here, DataMapper would
  # search for an :author relationship in Authorship.
  # Since there is no such relationship, that would
  # fail. By using :via => :person, we can instruct
  # DataMapper to use that relationship instead of
  # the :author default.

  has n, :authors, 'Person', :through => :authorships, :via => :person
end

class Person
  include DataMapper::Resource

  property id, Serial
end

class Authorship
  include DataMapper::Resource

  belongs_to :post,   :key => true  # 'post_id'   is part of the CPK
  belongs_to :person, :key => true  # 'person_id' is part of the CPK
end

Adding Conditions to Associations

If you want to order the association, or supply a scope, you can just pass in the options…

1
2
3
4
5
6
class Post
  include DataMapper::Resource

  has n, :comments, :order => [ :published_on.desc ], :rating.gte => 5
  # Post#comments will now be ordered by published_on, and filtered by rating > 5.
end

Finders off Associations

When you call an association off of a model, internally DataMapper creates a Query object which it then executes when you start iterating or call length off of. But if you instead call .all or .first off of the association and provide it the exact same arguments as a regular all and first, it merges the new query with the query from the association and hands you back a requested subset of the association’s query results.

In a way, it acts like a database view in that respect.

1
2
3
4
5
@post = Post.first
@post.comments                                                    # returns the full association
@post.comments.all(:limit => 10, :order => [ :created_at.desc ])  # return the first 10 comments, newest first
@post.comments(:limit => 10, :order => [ :created_at.desc ])      # alias for #all, you can pass in the options directly
@post.comments.popular                                            # Uses the 'popular' finder method/scope to return only highly rated comments

Querying via Relationships

Sometimes it’s desirable to query based on relationships. DataMapper makes this as easy as passing a hash into the query conditions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# find all Posts with a Comment by the user
Post.all(:comments => { :user => @user })
# in SQL => SELECT * FROM "posts" WHERE "id" IN
#     (SELECT "post_id" FROM "comments" WHERE "user_id" = 1)

# This also works (which you can use to build complex queries easily)
Post.all(:comments => Comment.all(:user => @user))
# in SQL => SELECT * FROM "posts" WHERE "id" IN
#     (SELECT "post_id" FROM "comments" WHERE "user_id" = 1)

# Of course, it works the other way, too
# find all Comments on posts with DataMapper in the title
Comment.all(:post => { :title.like => '%DataMapper%' })
# in SQL => SELECT * from "comments" WHERE "post_id" IN
#     (SELECT "id" FROM "posts" WHERE "title" LIKE '%DataMapper%')

DataMapper accomplishes this (in sql data-stores, anyway) by turning the queries across relationships into sub-queries.