It’s common in Rails to use a has_many :through
relationship to model User/Group Memberships.
Sometimes we have extra data in the join that we would like to make use of, but getting that
data in there can be combersome depending on our approach. For example, given the
following diagram and schema:
ActiveRecord::Schema.define(:version => 20120324170519) do
create_table "groups", :force => true do |t|
t.string "name"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "memberships", :force => true do |t|
t.integer "user_id"
t.integer "group_id"
t.string "role"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "users", :force => true do |t|
t.string "name"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
end
We might deal directly with the join table to assign our additonal data.
@user = User.create(name: 'User 1')
@user = Group.create(name: 'Group 1')
@membership = Membership.create do |m|
m.user = @user
m.group = @group
m.role = 'admin'
end
@user.admin? # => true
@user.editor? # => false
There’s a better way to pull this off …
@group.admins << @user
@user.admin? # => true
@user.editor? # => false
And this is how it’s done …
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
def admin?
memberships.where(:role => 'admin').first
end
def editor?
memberships.where(:role => 'editor').first
end
end
class Membership < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
has_many :admins, :through => :memberships, :source => :user,
:conditions => "memberships.role = 'admin'" do
def <<(admin)
proxy_association.owner.memberships.create(:role => 'admin', :user => admin)
end
end
has_many :editors, :through => :memberships, :source => :user,
:conditions => "memberships.role = 'editor'" do
def <<(editor)
proxy_association.owner.memberships.create(:role => 'editor', :user => editor)
end
end
end
We’re defining an extension on our group’s has_many
association which overrides
the <<
method on that collection. We then tell the proxy association’s owner
(which is our group object) to create the user/group join record, but with an additional
role assignment of ‘admin’.
@group.admins << @user
@user.admin? # => true
@user.editor? # => false
Pretty expressive, thanks to ActiveRecord!
require 'test_helper'
class GroupTest < ActiveSupport::TestCase
setup do
@user_1 = User.create(name: 'User 1')
@user_2 = User.create(name: 'User 2')
@user_3 = User.create(name: 'User 3')
@group = Group.create(name: 'Group 1')
end
test "No Memberships" do
assert_equal @user_1.memberships.count, 0
end
test "@group.users << @user_1 sets nil role on membership" do
@group.users << @user_1
assert_equal @user_1.memberships.count, 1
assert_equal @user_1.memberships.first.role, nil
end
test "@group.admins << @user_2 sets 'admin' role on membership" do
@group.admins << @user_2
assert_equal @user_2.memberships.count, 1
assert_equal @user_2.memberships.first.role, 'admin'
end
test "@group.editors << @user_3 sets 'editor' role on membership" do
@group.editors << @user_3
assert_equal @user_3.memberships.count, 1
assert_equal @user_3.memberships.first.role, 'editor'
end
teardown do
User.delete_all
Group.delete_all
Membership.delete_all
end
end