Katie
Katie

Reputation: 498

Accessing the associated join model when iterating through a has_many :through association

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

Answers (3)

sandre89
sandre89

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

Azolo
Azolo

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

Marcus Ilgner
Marcus Ilgner

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

Related Questions