Reputation: 14305
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:
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
has_many :conversation_items, dependent: :destroy
belongs_to :sender, polymorphic: true
belongs_to :recipient, polymorphic: true
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
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
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