Dennis Hackethal
Dennis Hackethal

Reputation: 14305

Rails - Polymorphic associations fail

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?

Upvotes: 1

Views: 661

Answers (2)

Sean Hill
Sean Hill

Reputation: 15056

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

Upvotes: 2

R Milushev
R Milushev

Reputation: 4315

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 :)

Upvotes: -1

Related Questions