Has_many :through


#1

I have the following (simple) scenario in a community project:

There is a simple messaging system where users can send messages to one
or many other users (comparable to email). So a message has one user as
sender and many users as recipients. I’ve modeled it like this:

CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
login VARCHAR(80) NOT NULL,

primary key (id),
INDEX (id)
) engine = InnoDB;

create table messages (
id INT NOT NULL AUTO_INCREMENT,
user_id int not null,
subject varchar(255),
body text,
primary key(id)
) engine = InnoDB, character set utf8;

create table message_users (
id INT NOT NULL AUTO_INCREMENT,
message_id int not null,
user_id int not null,
primary key(id)
) engine = InnoDB, character set utf8;

and in Rails:

class Message < ActiveRecord::Base
belongs_to :user # sender
has_many :message_users
has_many :users, :through => :message_user #recipients
end

class MessageRecipient < ActiveRecord::Base
belongs_to :message
belongs_to :user
end

class User < ActiveRecord::Base
has_many :message_users
has_many :messages, :through => :message_users
end

What I don’t like here is that a message has message.user as sender and
message.users as recipients. I would like to be able to call something
like this:

message.sender
message.recipients
user.messages
message.recipients << u

Another problem might be that user.messages is not clearly defined in my
code as it could be all messages the user has sent or all messages that
a user has received.

How can I achieve a better solution? Maybe you can help me out…


#2

Whoops, I knew I’d forget something… change this line in the User
class to add the foreign_key.

has_and_belongs_to_many :received_messages, :class_name => ‘Message’,
:foreign_key => :recipient_id


#3

I think this is a strong case for habtm, rather than has_many :through.
I’m normally a big fan of :through and use it often, but I’ve come to
realize that sometimes, habtm really is much more appropriate.

First, I’d rename your message_users table to messages_recipients (note:
both message and recipient are plural), and change the user_id column in
that table to recipient_id. Then…
(disclaimer: this code is untested, so there may be some typos and other
goofs)

class Message < ActiveRecord::Base
belongs_to :sender, :class_name => ‘User’
has_and_belongs_to_many :recipients, :class_name => ‘User’
end

class User < ActiveRecord::Base
has_many :sent_messages, :class_name => ‘Message’
has_and_belongs_to_many :received_messages, :class_name => ‘Message’
end

now…
@user.sent_messages
@user.received_messages
@message.sender
@message.recipients

voila!

get rid of your MessageRecipient class. You don’t need it.


#4

Jon G. wrote:

Whoops, I knew I’d forget something… change this line in the User
class to add the foreign_key.

has_and_belongs_to_many :received_messages, :class_name => ‘Message’,
:foreign_key => :recipient_id

Hmmm. That didn’t work for me. user.received_messages caused a look for
table messages_users which is not there.

Star


#5

I’m wondering why the has_many :through? Your join model has no extra
attributes at all. Why not just

Message
belongs_to :sender
has_and_belongs_to_many :recipients

User
has_many :sent_messages, :class_name => ‘Message’
has_and_belongs_to_many :received_messages, :class_name => ‘Message’

This gives you all the scenarios you desired and appears to be much
simpler than the path you are currently going down. I’d love to hear
other options using has_many :through with more attributes on the
join model.

-Michael
http://javathehutt.blogspot.com


#6

has_and_belongs_to_many automatically assumes that the join table for
the association is named like this.

tablename1_tablename2

where tablename1 is the table that comes first alphabetically. So for
a has_anf_belong_to_many association, you have to rename your
message_users table to messages_users, OR:

has_and_belongs_to_many received_messages, :class_name =>
‘Message’, :foreign_key => :recipient_id, :join_table =>
“message_users”

On 7 Mrz., 13:16, Star B. removed_email_address@domain.invalid


#7

Star B. wrote:

Jon G. wrote:

Whoops, I knew I’d forget something… change this line in the User
class to add the foreign_key.

has_and_belongs_to_many :received_messages, :class_name => ‘Message’,
:foreign_key => :recipient_id

Hmmm. That didn’t work for me. user.received_messages caused a look for
table messages_users which is not there.

Star

The following ist working via habtm:

create table messages_users (
message_id int not null,
user_id int not null,

primary key(message_id, user_id)

) engine = InnoDB, character set utf8;

class Message < ActiveRecord::Base
belongs_to :sender, :class_name => ‘User’
has_and_belongs_to_many :recipients, :class_name => ‘User’,
:foreign_key => ‘message_id’
end

class User < ActiveRecord::Base
has_many :sent_messages, :class_name => ‘Message’
has_and_belongs_to_many :received_messages, :class_name => ‘Message’,
:foreign_key => :user_id
end

Puuuh. Thanks for your advice.


#8

Hi Star, you should be able to do things that you need to do. The
:through allows you to have alot more flexibility in my opinion in the
event that you need to add additional field(s) to the join model.
Thus, you should be able to do the following with your current models:

message.user
message.message_users
message.users
message.users << user1
user.message_users
user.messages
user.messages << message1

Please revisit the AWDwRv2 for complete list of dynamically generated
methods using :through.

Good luck,

-Conrad


#9

Hey Conrad, list…

So while it’s true that using has_many :through does give you that
flexibility the fact that you now need to find a model for that join
table adds extra complexity to your domain model that you wouldn’t
otherwise have with a standard habtm relationship. But your point is
well taken. If you think it’s quite possible for your join table to
have additional attributes in the future making it a
has_many :through from the start saves you the pain of a data
migration later. But doing so as a manner of standard practice seems
to be overkill IMHO.

Best,

-Michael
http://javathehutt.blogspot.com


#10

Your tables and classes look correct to me. Assuming that you are
creating your messages properly by adding the correct sender and
recipients your data should be returned correctly when you access
those relations. Write a unit test and see what is in the DB after
you send a single message so you can be sure things are behaving as
you suspect.

-Michael
http://javathehutt.blogspot.com


#11

I’ve implemented

class Message < ActiveRecord::Base
belongs_to :sender, :class_name => ‘User’
has_and_belongs_to_many :recipients, :class_name => ‘User’
end

class User < ActiveRecord::Base
has_many :sent_messages, :class_name => ‘Message’
has_and_belongs_to_many :received_messages, :class_name => ‘Message’
end

with

CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
login VARCHAR(80) NOT NULL,

primary key (id),
INDEX (id)
) engine = InnoDB;

create table messages (
id INT NOT NULL AUTO_INCREMENT,
user_id int not null,
subject varchar(255),
body text,
created_at DATETIME default NULL,
modified_at DATETIME default NULL,

primary key(id)

) engine = InnoDB, character set utf8;

create table messages_users (
message_id int not null,
user_id int not null,

primary key(message_id, user_id)

) engine = InnoDB, character set utf8;

First everything seemed fine. But user.received_messages delivers
message objects that are DEFINITELY NOT in the database (wrong sender
ID)!! I had only one singel message in the DB, but

user.received_messages.first yields another object than Message.find
:first

Strange, strange!!! A bug maybe?