Getting Data From a Join Table in Rails

A friend asked in my Ruby Facebook group about a problem he was having that I feel I’ve encountered before (or will be encountering in the future). It’s about having to retrieve data that is associated with a join table.

Here’s the question:

I have set ActiveRecords models like these

I want to do something like current_user.boards.first.role which I expect to load the membership details of current_user AND the first board. Do you have any idea how can I achieve this? Do you suggest another approach?

In other words, he wants to be able to get at the current_user’s boards and retrieve the role assigned to the first board.

At first it was a bit difficult to search google for the corrent keywords, but there’s StackOverflow to the rescue. There needs to be a custom SQL select query to expose the role_mask attribute of the join table

I felt that it was a very interesting question, so I made a sample app for him to show the technique. It can be found here: https://github.com/parasquid/reach-into-habtm-example/

For the lazy and impatient:

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :memberships
  has_many :boards, through: :memberships, :select => 'memberships.role_mask as role_mask'
end
# spec/models/membership_spec.rb
require 'spec_helper'

MAX_USERS = 2
MAX_BOARDS = 5

describe Membership do
  context 'access through a user instance' do
    before :each do
      1.upto MAX_USERS do
        User.create
      end

      1.upto MAX_BOARDS do
        Board.create
      end

      # associate two boards to the first user
      user1 = User.first
      user1.memberships.create(role_mask: 1, board: Board.first)
      user1.memberships.create(role_mask: 4, board: Board.last)

      # associate two boards to the second user in reverse order
      user2 = User.last
      user2.memberships.create(role_mask: 4, board: Board.first)
      user2.memberships.create(role_mask: 1, board: Board.last)
    end

    context 'first user' do
      let(:current_user) { User.first }

      it 'returns the current user\'s board\'s membership role' do
        expect(current_user.boards.first.role).to eq :admin
        expect(current_user.boards.last.role).to eq :member
      end
    end

    context 'second user' do
      let(:current_user) { User.last }

      it 'returns the current user\'s board\'s membership role' do
        expect(current_user.boards.first.role).to eq :member
        expect(current_user.boards.last.role).to eq :admin
      end
    end

  end
end

And of course, as Milad’s comment (and the StackOverflow guy) says, the :select hash is already deprecated in Rails 4 and you’d need to do use a lambda instead:

has_many :boards,
         -> { select 'boards.*, memberships.role_mask as role_mask'},
         :through => :memberships
comments powered by Disqus