Datamapper is an alternative ORM for various ruby projects, and can work as an ORM replacement for ActiveRecord in Ruby on Rails. The most pervasive arguments for using Datamapper over AR is the easier handling of legacy databases and the intelligent deconstruction of N+1 queries, and the fact that functionality is splitted into small plugins, so for simple projects it does not weigh down the framework codebase with unused code.
One other strength for me is the auto-migration part: You do not have (but you are able) to add separate migrations that pretty much reflect what you have configured in the models already. It takes the configuration of the model properties and creates the tables from that (destructive, non-destructive: your choice). This means that the control of the properties of a data model is no longer handled by a separate script, but by the definitions of the structure of the models itself. This can optionally include referential integrity in the underlying database, too.
Also, the documentation is really good. There are whole chapters handling understanding of HABTM (has n, through: …, self-referential HABTM relationships et al). But one chapter left me stumped a bit:
How to customize a relationship?
Maybe it was me skimming over the surface of the documentation, maybe my current needs were not exactly covered, but reading the fine manual was not quite enough for me. I still managed to create classes that wouldn’t play nice.
The problem:
I had a simple model, User:
1 2 3 4 5 6 7 8 |
|
And another simple model, Message:
1 2 3 4 5 6 7 8 9 |
|
Now, I wanted to add a relation from User to Message as follows:
- a
User
has many sent messages - a
Message
belongs to one sender
Doing it as a normal relation would be simple, as follows:
First, I change the User model:
1 2 3 4 5 6 7 8 9 10 |
|
Then the Message model:
1 2 3 4 5 6 7 8 9 10 11 |
|
So far, so easy. Now the more complex part: I want to have another, more complex relation:
- a Message can have many recipients
- a User can have many received messages
Now, as you can see immediately, this is going to be a real HABTM-relationship. But when I started to add the relation to the User model, I saw oddities immediately:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
At this point I decided to just rename the first relation to what I already suggested: sender and sent_messages:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
As an explanation: If I rename a relation, but there is no model that corrensponds to the singularized name (e.g. Sender), Datamapper will complain when migrating or instantiating the relationship. So, I have to tell datamapper to use the ‘Message’ model for the relation ‘sent_messages’, and the ‘User’ model for the ‘sender’ relation. The constraint ‘child_key’ helps with selecting the correct foreign key field in the child model - the side that has the ‘belongs_to’ association added.
And the second association will add a table messages_users with the fields user_id and message_id as primary key. This should be sufficient …
But no, I wanted a bit more: first, I wanted to store the time the message was read by each recipient, and then I wanted to have nice names for the associations without using (ugly) proxy methods.
The first part is easy: I will add another model, Notification, that keeps the recipient/message relation and has a ‘read’ DateTime
field as payload:
1 2 3 4 5 6 7 8 9 |
|
The key: true
constraint on the property means that both fields are part of the primary key. Now let’s adapt the other two classes. User first:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Message second:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
The through:
constraint tells Datamapper to use the notifications
table as underlying n:m relationship table.
But now I wanted to make it really neat: from Clean Code we know that having methods that are named for their purpose create more value that cryptically named ones. So, I decided to rename those associations to reflect theirs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
… to find out that this does not work.
First, the notification tables looked like this:
1 2 3 4 5 |
|
Trying to use the models on the console lead to messages like:
property or constraint recipient not found on model User
Well. Erm. No good. But there is a solution already built in, but the documentation all but skips it:
[…]we will use :via to be able to provide “better” names[…]
Solution:
But the rest of the example took me a while to figure out. I will let the finished example speak for itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 |
|
So, what does :via
tell Datamapper? Fairly simple: it says: “ The reverse relation of this relationship is called “:message” in the “Notification” model”.
Reading the code at the documentation example first, I tried the following:
- Let
:via
point to the reverse relation in the other model (User
orMessage
, notNotification
) - Let the
:notification
relationship know that the child key is:message_id
for:received_messages
inNotification
(and:user_id
for:recipients
in Message) - Both of the above
- …
It took me the better part of two hours to read through the code and realize where I made the wrong assumption. But now everything works.
Conclusio
If you want to customize a HABTM-Association in Datamapper, make sure that:
-
The link model (here: Notification) uses the derived name for the association as is standard: “User” is related by “belongs_to :user”, “Message” by “belongs_to :message” and so on.
-
The other models’ relations know what the relationship’s reverse is named: In the “User” model, “received_messages” is defined as
has n, :received_messages, 'Message', through: :notifications, via: :message
where
- “:received_messages” is the relationship name
- “‘Message’” the name of the model that is linked via this relationship, and
- “via: :message” denotes the reverse of the association in the “link” model (here: “Notification”).
I hope that this helps someone to save some time and helps understanding.