Reputation: 498
I have a feeling this is a pretty basic question, but for some reason I'm stumped by it (Rails newbie) and can't seem to find the answer (which may be I'm not searching properly).
So I have a basic has_many :through relationship like this:
class User < ApplicationRecord
has_many :contacts, through :user_contacts
class Contact < ApplicationRecord
has_many :users, through :user_contacts
In users/show.html.erb I'm iterating through a single user's contacts, like:
<% @user.contacts.each do |c| %>
<%= c.name %>
<% end %>
Now inside of that each loop, I want to access the user_contact join model that's associated with the given user and contact in order to display the created_at timestamp that indicates when the user <--> contact relationship was made.
I know I could just do a UserContact.find call to look up the model in the database by the user_id and contact_id but somehow this feels superfluous. If I understand correctly how this works (it's entirely possible I don't) the user_contact model should have already been loaded when I loaded the given user and its contacts from the database already. I just don't know how to properly access the correct model. Can someone help with the correct syntax?
Upvotes: 3
Views: 1401
Reputation: 5898
Use .joins
and .select
in this way:
@contacts = current_user.contacts.joins(user_contacts: :users).select('contacts.*, user_contacts.user_contact_attribute_name as user_contact_attribute_name')
Now, inside @contacts.each do |contact|
loop, you can call contact.user_contact_attribute_name
.
It looks weird because contact
doesn't have that user_contact_attribute_name
, only UserContact
does, but the .select
portion of the query will make that magically available to you on each contact
instance.
The contacts.*
portion is what tells the query to make all contact
's attributes available as well.
Upvotes: 1
Reputation: 4383
First of all, the has_many :things, through: :other_things
clause is going to look for the other_things
relationship to find :things
.
Think of it as a method call of sorts with magic built in to make it performant in SQL queries. So by using a through
clause you're more or less doing something like:
def contacts
user_contacts.map { |user_contact| user_contact.contacts }.flatten
end
The context of the user_contacts
is completely lost.
Since it looks like user_contacts
is a one-to-one join. It would be easier to do something like this:
<% @user.user_contacts.each do |user_contact| %>
<%= user_contact.contact.name %>
<% end %>
Also since you're new to Rails it's worth mentioning that to load those records without an N+1 query you can do something like this in your controller:
@user = User.includes(user_contacts: [:contacts]).find(params[:id])
Upvotes: 2
Reputation: 7211
Actually the join model will not have been loaded yet: ActiveRecord takes the through
specification to build its SQL JOIN
statements for querying the correct Contact
records but effectively will only instantiate those.
Assuming you have a UserContact
model, you could do sth like this:
@user.user_contacts.includes(:contact).find_each do |uc|
# now you can access both join model and contact without additional queries to the DB
end
If you want to keep things readable without cluttering your code with uc.contact.something
, you can set up delegations inside the UserContact
model that delegate some properties to contact
or user
respectively. For example this
class UserContact < ActiveRecord::Base
belongs_to :user
belongs_to :contact
delegate :name, to: :contact, prefix: true
end
would allow you to write
uc.contact_name
Upvotes: 4