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 class Post
 2   include DataMapper::Resource
 3 
 4   property :id, Serial
 5 
 6   has n, :comments
 7 end
 8 
 9 class Comment
10   include DataMapper::Resource
11 
12   property :id,     Serial
13   property :rating, Integer
14 
15   belongs_to :post  # defaults to :required => true
16 
17   def self.popular
18     all(:rating.gt => 3)
19   end
20 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 class Photo
 2   include DataMapper::Resource
 3 
 4   property :id, Serial
 5 
 6   has n, :taggings
 7   has n, :tags, :through => :taggings
 8 end
 9 
10 class Tag
11   include DataMapper::Resource
12 
13   property :id, Serial
14 
15   has n, :taggings
16   has n, :photos, :through => :taggings
17 end
18 
19 class Tagging
20   include DataMapper::Resource
21 
22   belongs_to :tag,   :key => true
23   belongs_to :photo, :key => true
24 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 class Tagging
 2   include DataMapper::Resource
 3 
 4   property :id, Serial
 5   
 6   property :tag_id,          :unique_index => :uniqueness, :required => true
 7   property :tagged_photo_id, :unique_index => :uniqueness, :required => true
 8 
 9   belongs_to :tag
10   belongs_to :tagged_photo, 'Photo'
11 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 # When auto_migrate! is being called, the following model
 2 # definitions will create an
 3 #
 4 #  ArticleCategory
 5 #
 6 # model that will be automigrated and that will act as the join
 7 # model. DataMapper just picks both model names, sorts them
 8 # alphabetically and then joins them together. The resulting
 9 # storage name follows the same conventions it would if the
10 # model had been declared traditionally.
11 #
12 # The resulting model is no different from any traditionally
13 # declared model. It contains two belongs_to relationships
14 # pointing to both Article and Category, and both underlying
15 # child key properties form the composite primary key (CPK)
16 # of that model. DataMapper uses consistent naming conventions
17 # to infer the names of the child key properties. Since it's
18 # told to link together an Article and a Category model, it'll
19 # establish the following relationships in the join model.
20 #
21 #  ArticleCategory.belongs_to :article,  'Article',  :key => true
22 #  ArticleCategory.belongs_to :category, 'Category', :key => true
23 #
24 # Since every many to many relationship needs a one to many
25 # relationship to "go through", these also get set up for us.
26 #
27 #  Article.has n, :article_categories
28 #  Category.has n, article_categories
29 #
30 # Essentially, you can think of ":through => Resource" being
31 # replaced with ":through => :article_categories" when DM
32 # processes the relationship definition.
33 #
34 # This also means that you can access the join model just like
35 # any other DataMapper model since there's really no difference
36 # at all. All you need to know is the inferred name, then you can
37 # treat it just like any other DataMapper model.
38 
39 class Article
40   include DataMapper::Resource
41 
42   property :id, Serial
43 
44   has n, :categories, :through => Resource
45 end
46 
47 class Category
48   include DataMapper::Resource
49 
50   property :id, Serial
51 
52   has n, :articles, :through => Resource
53 end
54 
55 # create two resources
56 article  = Article.create
57 category = Category.create
58 
59 # link them by adding to the relationship
60 article.categories << category
61 article.save
62 
63 # link them by creating the join resource directly
64 ArticleCategory.create(:article => article, :category => category)
65 
66 # unlink them by destroying the related join resource
67 link = article.article_categories.first(:category => category)
68 link.destroy
69 
70 # unlink them by destroying the join resource directly
71 link = ArticleCategory.get(article.id, category.id)
72 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 class Person
 2   include DataMapper::Resource
 3 
 4   property :id,    Serial
 5   property :name , String, :required => true
 6 
 7   has n, :friendships, :child_key => [ :source_id ]
 8   has n, :friends, self, :through => :friendships, :via => :target
 9 end
10 
11 class Friendship
12   include DataMapper::Resource
13 
14   belongs_to :source, 'Person', :key => true
15   belongs_to :target, 'Person', :key => true
16 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 class Person
 2 
 3   # ...
 4 
 5   # Since the foreign key pointing to Person isn't named 'person_id',
 6   # we need to override it by specifying the :child_key option. If the
 7   # Person model's key would be something different from 'id', we would
 8   # also need to specify the :parent_key option.
 9 
10   has n, :friendships, :child_key => [ :source_id ]
11 
12 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 class Person
 2 
 3   # ...
 4 
 5   has n, :friendships, :child_key => [ :source_id ]
 6 
 7   # We name the relationship :friends cause that's the original intention
 8   #
 9   # The target model of this relationship will be the Person model as well,
10   # so we can just pass self where DataMapper expects the target model
11   # You can also use Person or 'Person' in place of self here. If you're
12   # constructing the options programmatically, you might even want to pass
13   # the target model using the :model option instead of the 3rd parameter.
14   #
15   # We "go through" the :friendship relationship in order to get at the actual
16   # friends. Since we named our relationship :friends, DataMapper assumes
17   # that the Friendship model contains a :friend relationship. Since this
18   # is not the case in our example, because we've named the relationship
19   # pointing to the actual friend person :target, we have to tell DataMapper
20   # to use that relationship instead, when looking for the relationship to
21   # piggy back on. We do so by passing the :via option with our :target
22 
23   has n, :friends, self, :through => :friendships, :via => :target
24 
25 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 class Person
 2 
 3   class Link
 4 
 5     include DataMapper::Resource
 6 
 7     storage_names[:default] = 'people_links'
 8 
 9     # the person who is following someone
10     belongs_to :follower, 'Person', :key => true
11 
12     # the person who is followed by someone
13     belongs_to :followed, 'Person', :key => true
14 
15   end
16 
17   include DataMapper::Resource
18 
19   property :id,   Serial
20   property :name, String, :required => true
21 
22 
23   # If we want to know all the people that John follows, we need to look
24   # at every 'Link' where John is a :follower. Knowing these, we know all
25   # the people that are :followed by John.
26   #
27   # If we want to know all the people that follow Jane, we need to look
28   # at every 'Link' where Jane is :followed. Knowing these, we know all
29   # the people that are a :follower of Jane.
30   #
31   # This means that we need to establish two different relationships to
32   # the 'Link' model. One where the person's role is :follower and one
33   # where the person's role is to be :followed by someone.
34 
35   # In this relationship, the person is the follower
36   has n, :links_to_followed_people, 'Person::Link', :child_key => [:follower_id]
37 
38   # In this relationship, the person is the one followed by someone
39   has n, :links_to_followers, 'Person::Link', :child_key => [:followed_id]
40 
41 
42   # We can then use these two relationships to relate any person to
43   # either the people followed by the person, or to the people this
44   # person follows.
45 
46   # Every 'Link' where John is a :follower points to a person that
47   # is followed by John.
48   has n, :followed_people, self,
49     :through => :links_to_followed_people, # The person is a follower
50     :via     => :followed
51 
52   # Every 'Link' where Jane is :followed points to a person that
53   # is one of Jane's followers.
54   has n, :followers, self,
55     :through => :links_to_followers, # The person is followed by someone
56     :via     => :follower
57 
58   # Follow one or more other people
59   def follow(others)
60     followed_people.concat(Array(others))
61     save
62     self
63   end
64 
65   # Unfollow one or more other people
66   def unfollow(others)
67     links_to_followed_people.all(:followed => Array(others)).destroy!
68     reload
69     self
70   end
71 
72 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 class Person
 2   include DataMapper::Resource
 3 
 4   has 1, :profile
 5 end
 6 
 7 class Profile
 8   include DataMapper::Resource
 9 
10   belongs_to :person
11 end
12 
13 # Assigning a resource to a one-to-one relationship
14 
15 person  = Person.create
16 person.profile = Profile.new
17 person.save
18 
19 # Assigning a resource to a many-to-one relationship
20 
21 profile = Profile.new
22 profile.person = Person.create
23 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 post = Post.get(1)  # find a post to add a comment to
 2 
 3 # This will add a new but not yet saved comment to the collection
 4 comment = post.comments.new(:subject => 'DataMapper ...')
 5 
 6 # Both of the following calls will actually save the comment
 7 post.save     # This will save the post along with the newly added comment
 8 comment.save  # This will only save the comment
 9 
10 # This will create a comment, save it, and add it to the collection
11 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 post.comments << comment  # append an already existing comment
2 
3 # Both of the following calls will actually save the comment
4 post.save           # This will save the post along with the newly added comment
5 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 class Person
 2   include DataMapper::Resource
 3   property :id, Serial
 4   has n, :tasks
 5 end
 6 
 7 class Task
 8   include DataMapper::Resource
 9   property :id, Serial
10   belongs_to :person
11 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 ree-1.8.7-2010.02 > p = Person.create
 2  => #<Person @id=1>
 3 ree-1.8.7-2010.02 > t = Task.create :person => p
 4  => #<Task @id=1 @person_id=1>
 5 ree-1.8.7-2010.02 > p.tasks
 6  => [#<Task @id=1 @person_id=1>]
 7 ree-1.8.7-2010.02 > u = Task.create :person => p
 8  => #<Task @id=2 @person_id=1>
 9 ree-1.8.7-2010.02 > p.tasks
10  => [#<Task @id=1 @person_id=1>]
11 ree-1.8.7-2010.02 > p.tasks.reload
12  => [#<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 class Blog
 2   include DataMapper::Resource
 3 
 4   # The rules described below apply equally to definitions
 5   # of one-to-one relationships. The only difference being
 6   # that those would obviously only point to a single resource.
 7 
 8   # However, many-to-many relationships don't accept all the
 9   # options described below. They do support specifying the
10   # target model, like we will see below, but they do not support
11   # the :parent_key and the :child_key options. Instead, they
12   # support another option that's available to many-to-many
13   # relationships exclusively. This option is called :via, and
14   # will be explained in more detail in its own paragraph below.
15 
16   # - This relationship points to multiple resources
17   # - The target resources will be instances of the 'Post' model
18   # - The local parent_key is assumed to be 'id'
19   # - The remote child_key is assumed to be 'blog_id'
20   #   - If the child model (Post) doesn't define the 'blog_id'
21   #     child key property either explicitly, or implicitly by
22   #     defining it using a belongs_to relationship, it will be
23   #     established automatically, using the defaults described
24   #     here ('blog_id').
25 
26   has n, :posts
27 
28   # The following relationship definition has the exact same
29   # effect as the version above. It's only here to show which
30   # options control the default behavior outlined above.
31 
32   has n, :posts, 'Post',
33     :parent_key => [ :id ],      # local to this model (Blog)
34     :child_key  => [ :blog_id ]  # in the remote model (Post)
35 
36 end
37 
38 class Post
39   include DataMapper::Resource
40 
41   # - This relationship points to a single resource
42   # - The target resource will be an instance of the 'Blog' model
43   # - The locally established child key will be named 'blog_id'
44   #   - If a child key property named 'blog_id' is already defined
45   #     for this model, then that will be used.
46   #   - If no child key property named 'blog_id' is already defined
47   #     for this model, then it gets defined automatically.
48   # - The remote parent_key is assumed to be 'id'
49   #   - The parent key must be (part of) the remote model's key
50   # - The child key is required to be present
51   #   - A parent resource must exist and be assigned, in order
52   #     for this resource to be considered complete / valid
53 
54   belongs_to :blog
55 
56   # The following relationship definition has the exact same
57   # effect as the version above. It's only here to show which
58   # options control the default behavior outlined above.
59   #
60   # When providing customized :parent_key and :child_key options,
61   # it is not necessary to specify both :parent_key and :child_key
62   # if only one of them differs from the default conventions.
63   #
64   # The :parent_key and :child_key options both accept arrays
65   # of property name symbols. These should be the names of
66   # properties being (at least part of) a key in either the
67   # remote (:parent_key) or the local (:child_key) model.
68   #
69   # If the parent resource need not be present in order for this
70   # model to be considered complete, :required => false can be
71   # passed to stop DataMapper from establishing checks for the
72   # presence of the attribute value.
73 
74   belongs_to :blog, 'Blog',
75     :parent_key => [ :id ],       # in the remote model (Blog)
76     :child_key  => [ :blog_id ],  # local to this model (Post)
77     :required   => true           # the blog_id must be present
78 
79 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 class Post
 2   include DataMapper::Resource
 3 
 4   belongs_to :blog, :key => true  # 'blog_id' is the primary key
 5 end
 6 
 7 class Person
 8   include DataMapper::Resource
 9 
10   property id, Serial
11 end
12 
13 class Authorship
14   include DataMapper::Resource
15 
16   belongs_to :post,   :key => true  # 'post_id'   is part of the CPK
17   belongs_to :person, :key => true  # 'person_id' is part of the CPK
18 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 class Post
 2   include DataMapper::Resource
 3 
 4   property :id, Serial
 5 
 6   has n, :authorships
 7 
 8   # Without the use of :via here, DataMapper would
 9   # search for an :author relationship in Authorship.
10   # Since there is no such relationship, that would
11   # fail. By using :via => :person, we can instruct
12   # DataMapper to use that relationship instead of
13   # the :author default.
14 
15   has n, :authors, 'Person', :through => :authorships, :via => :person
16 end
17 
18 class Person
19   include DataMapper::Resource
20 
21   property id, Serial
22 end
23 
24 class Authorship
25   include DataMapper::Resource
26 
27   belongs_to :post,   :key => true  # 'post_id'   is part of the CPK
28   belongs_to :person, :key => true  # 'person_id' is part of the CPK
29 end

Adding Conditions to Associations

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

1 class Post
2   include DataMapper::Resource
3 
4   has n, :comments, :order => [ :published_on.desc ], :rating.gte => 5
5   # Post#comments will now be ordered by published_on, and filtered by rating > 5.
6 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 @post = Post.first
2 @post.comments                                                    # returns the full association
3 @post.comments.all(:limit => 10, :order => [ :created_at.desc ])  # return the first 10 comments, newest first
4 @post.comments(:limit => 10, :order => [ :created_at.desc ])      # alias for #all, you can pass in the options directly
5 @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 # find all Posts with a Comment by the user
 2 Post.all(:comments => { :user => @user })
 3 # in SQL => SELECT * FROM "posts" WHERE "id" IN
 4 #     (SELECT "post_id" FROM "comments" WHERE "user_id" = 1)
 5 
 6 # This also works (which you can use to build complex queries easily)
 7 Post.all(:comments => Comment.all(:user => @user))
 8 # in SQL => SELECT * FROM "posts" WHERE "id" IN
 9 #     (SELECT "post_id" FROM "comments" WHERE "user_id" = 1)
10 
11 # Of course, it works the other way, too
12 # find all Comments on posts with DataMapper in the title
13 Comment.all(:post => { :title.like => '%DataMapper%' })
14 # in SQL => SELECT * from "comments" WHERE "post_id" IN
15 #     (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.