1

This should be easy to solve, but I am new to rails. Delving into the rails guide on associations right now, but constantly hitting roadblocks.

I have a user model that can send messages to other users. For that purpose, there is a conversation model for all the threads, and a conversation_item model for each individual message.

These are the associations right now:

User.rb

has_many :received_messages, as: :recipient, class_name: 'Conversation', foreign_key: :recipient_id
has_many :sent_messages, as: :sender, class_name: 'Conversation', foreign_key: :sender_id
has_many :received_messages, as: :recipient, class_name: 'Conversation_item', foreign_key: :recipient_id
has_many :sent_messages, as: :sender, class_name: 'Conversation_item', foreign_key: :sender_id

Conversation.rb

has_many :conversation_items, dependent: :destroy
belongs_to :sender, polymorphic: true
belongs_to :recipient, polymorphic: true

Conversation_item.rb

belongs_to :conversation
belongs_to :sender, polymorphic: true
belongs_to :recipient, polymorphic: true

I am trying to access a user through his conversation item like so:

<%= conversation_item.sender.inspect %>

This returns nil - the attribute sender does seem to work, but somehow doesn't contain anything. However, the conversation item does indeed have a sender_id.

What am I missing in the associations above? Or anywhere else?

Dennis Hackethal
  • 13,662
  • 12
  • 66
  • 115

2 Answers2

2

It seems to me that you do not need to use polymorphic relationships here. You should be able to do this:

class User < ActiveRecord::Base
  has_many :participations, class_name: 'Participant'
  has_many :conversations, through: :participations
  has_many :conversation_items, through: :conversations
end

class Participant < ActiveRecord::Base
  belongs_to :user
  belongs_to :conversation
end 

class Conversation < ActiveRecord::Base
  has_many :participants
  has_many :conversation_items
end

class ConversationItem < ActiveRecord::Base
  belongs_to :conversation
  belongs_to :sender, class_name: 'User', foreign_key: 'sender_id'
end

To me, this makes more sense as a domain model, where a conversation has many participants. You can attach the sender to the conversation item, and every participant on the conversation, who is not the sender is implicitly the recipient. It does not make sense that a conversation would have a sender and a receiver, since a conversation is a two-way, or multi-way, dialogue.

The reason that polymorphic relationships do not work here is that polymorphic relationships are for situations where a table could refer to different types of items. For example, let's say that your ConversationItem had many attachments, and attachments could also be applied to posts. You would have something like:

class ConversationItem < ActiveRecord::Base
  #...
  has_many :attachments, as: :attachable
  #...
end

class Post < ActiveRecord::Base
  has_many :attachments, as: :attachable
end

class Attachment < ActiveRecord::Base
  belongs_to :attachable, polymorphic: true
end

Where attachment would have the fields attachable_id and attachable_type. When you query attachments from conversation items, you would have a query like SELECT * FROM attachments where attachable_id = <id of the conversation item> and attachable_type = 'ConversationItem'. So polymorphic associations give you the ability to attach a model to many different types of other models.

In this case, you can see how it doesn't make sense to have polymorphic relationships, unless you were going to have many different types of models that send and receive conversation items because on your conversation_items table, you would have sender_type, sender_id, recipient_type, recipient_id.

Another issue that you have with your models is that you are trying to define the same relationship twice with different parameters. When you call has_many :foo, Rails is generating a bunch of different methods, like foos, foos=, and many more. If you call it again, you're just redefining those methods with the new parameters.

Followup

You don't necessarily need to have the Participant model. Having it gives you the ability to have multiple users participate, and it lets you avoid creating multiple relationships to same model. You can achieve the same thing with a has_and_belongs_to_many relationships, but you lose, should the need ever arise, the ability to attach extra attributes to the join table.

Say, for example, you wanted to allow people to exit a conversation without removing that they were once a participant, with a join model, you can add a boolean field call active, setting to false when someone exits. With HABTM, you couldn't do that. Having a participant leave would require that the pair be completely deleted from the join table. I usually prefer join models, like the Participation model here, since you never know how your schema may evolve.

That said, here is an example of the HABTM:

class User < ActiveRecord::Base
  has_and_belongs_to_many :conversations
end

class Conversation < ActiveRecord::Base
  has_and_belongs_to_many :users
end

Here you do not have a join model, but you will need to create a join table for this to work. An example of a migration for that would be:

create_table :conversations_users, :id => false do |t|
  t.integer :conversation_id
  t.integer :user_id
end

add_index :conversations_users, [:conversation_id, :user_id], unique: true

Where the name of the constituents of the join table are placed in alphabetical order, i.e. conversations_users, not users_conversations.

Further followup

If you use the Participant table, the foreign keys would be on participants and conversation_items.

participants
  user_id
  conversation_id

conversation_items
  conversation_id
  sender_id
Sean Hill
  • 14,978
  • 2
  • 50
  • 56
  • Thanks so far! I don't really get yet what I need the participant class for - and what attributes will it have? Just a user_id and a conversation_id? – Dennis Hackethal Dec 08 '12 at 20:32
  • So basically I would get rid of the participants' ids in the conversation and in the conversation_item and instead use the participant model with a user_id and a conversation_id? – Dennis Hackethal Dec 08 '12 at 20:45
  • To answer your first question, yes, you would have the `user_id` and `conversation_id` on the `Participation` model. To the second questions, yeah, that's what you're doing. It removes the need to create multiple relationships to the same models. See if my followup helps. – Sean Hill Dec 08 '12 at 20:57
  • Thanks for the follow up - I think we're close to the solution! :-) I think I'll take the participant approach as it provides plenty of flexibility - there seems to be a mistake with this syntax however: `belongs_to :sender, class: 'User'` - it gives me `Unknown key: class`. To what do I need to change that? – Dennis Hackethal Dec 08 '12 at 20:58
  • Yeah, I made a mistake. It should be `class_name:`. See my edits. – Sean Hill Dec 08 '12 at 21:09
  • Now this is strange - I have manually created a conversation, two participants and one conversation_item in the database to test everything. When I do `conversation.conversation_item.body` however, it says it doesn't know `conversation_item` - are we still missing something in the associations? Sorry for taking so much of your time. – Dennis Hackethal Dec 08 '12 at 21:25
  • Since Conversation has many conversation items, you won't have the `conversation_item` method. Rather, you will have `conversation_items`, which will return an `ActiveRecord::Relation`, which, for practical purposes, would be an array or conversation items. If you wanted a particular one, you would have to do `conversation.conversation_items.first` or `conversation.conversation_items[2]`. The reverse is true for conversation items. You would have `conversation_item.conversation`, which would return a conversation. – Sean Hill Dec 08 '12 at 21:30
  • 1
    Sorry, that was a lame typo. It works now! Thank you very much for your help and patience! I would love to give 100 upvotes, but can only give one. Best answer I got in a long time! – Dennis Hackethal Dec 08 '12 at 21:35
-1

You model's logic seems to be fine . My suggestion is the controller and view structure . When you create associations in Rails , it's common approach to nest resources (in your routes.rb):

resources :conversations do resources :conversation_items end

Then your Conversation_items controller should have something like this in new and create actions :

def new
  @conversation = Conversation.find(params[:conversation_id]
  @conversation_item = Conversation_item.new
end

def create
  @conversation = Conversation.find(params[:conversation_id]
  @conversation_item = @conversation.conversation_items.build(params[:conversation_items]
end

And finally , in your _form.html.erb (which serves both create and update actions):

  <%= form_for [@conversation, @conversation_item] do |f| %>

     # your code here

  <%= end %>

I was there , as you can see here ... It's bitter-sweet :)

Community
  • 1
  • 1
R Milushev
  • 4,295
  • 3
  • 27
  • 35
  • No, I don't think this is it. I have the resources as you described, and I am neither referring to the new nor to the create method right now. Just to the index method which already has conversations. – Dennis Hackethal Dec 08 '12 at 20:16