Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

DRYing up Many-to-Many Relationships in ActiveRecord

branch: master

Fetching latest commit…

Octocat-spinner-32-eaf2f5

Cannot retrieve the latest commit at this time

Octocat-spinner-32 app
Octocat-spinner-32 lib
Octocat-spinner-32 rails
Octocat-spinner-32 test
Octocat-spinner-32 .gitignore
Octocat-spinner-32 MIT-LICENSE
Octocat-spinner-32 README.markdown
Octocat-spinner-32 Rakefile
Octocat-spinner-32 init.rb
README.markdown

ActsAsJoinable

Like has_many_polymorphs but easier. Can handle double polymorphic associations with single table inheritance from the join model.

Install

sudo gem install acts-as-joinable

Usage

Dry Assocations with Zero Dependencies

Here's what you would write:

class Content < ActiveRecord::Base
  joins :assets
  joins :images
  joins_one :cover_image, :source => :image
end

class Page < Content
  joins :children, :as => :parent, :source => :content
  joins :parents, :as => :child, :source => :content
end

class Post < Content
  joins :parents, :as => :child, :source => :page
end

class Asset < ActiveRecord::Base
  joinable
end

class Image < Asset

end

Here's how you'd use it:

page = Page.create!(:title => "Home Page")
post = Post.create!(:title => "My first blog post")
image = Image.create!(:src => "http://imgur.com/123123.png")

page.children << post
post.cover_image = image

assert_equal post, page.children.first
assert_equal image, post.images.first
assert_equal image, post.assets.first

You can also create a simple group membership system no problem:

class User < ActiveRecord::Base
  joins :groups, :as => :child, :context => :membership
end

class Group < ActiveRecord::Base
  joins :members, :context => :membership, :source => :user
  joins_one :admin, :context => :membership, :value => :admin, :source => :user
  joins :board_of_directors, :context => :membership, :value => :board, :source => :user
end

basic_member  = User.create!(:position => "I'm an employee")
board_member  = User.create!(:position => "I'm on the board")
company       = Group.create!(:name => "A Company")

company.members << basic_member
company.board_of_directors << board_member

assert_equal 2, company.members.length
assert_equal 1, company.board_of_directors

What's it doing?

First, it has a generic join model. All join models are the same in the end, so there is no need to create extra tables for each. Maybe when you get 10,000,000 records you'll need to start creating tables for specific joins, but you don't need that by default. It increases the complexity of your application unnecessarily which makes it harder to extend and manage.

Instead, this creates a single join model that will solve for most of your cases (if there is a case it doesn't solve for, I'd love to know, it solves all of mine).

Here's the table for the built-in Relationship model:

create_table :relationships do |t|
  t.references :parent, :polymorphic => true
  t.references :child, :polymorphic => true
  t.string :context
  t.string :value
  t.integer :position
  t.timestamps
end

The features are:

  1. Double sided polymorphic associations. Which means you can tie any object to any other object.
  2. Built-in relationship directionality, similar to a Directed Acyclic Graph. So you can say the Post is parent of Image, since you usually attach an Image to a Post (not the other way around), so Image is child of Post. This means you have some sort of built in hierarchy.
  3. Context. You can create many-to-many relationships between the same models and call them different things. This is roughly equivalent to creating STI join models. This is useful for creating something like organizing Users of a Group into Roles.
  4. Position. You can sort the objects by relationship in primitive ways.

You can always add columns to the relationship table, but the foundation is set.

copyright @viatropos 2010

http://rors.org/2008/10/26/dont-escape-in-strings

  • Handle case where one side has_one and the other has_many
Something went wrong with that request. Please try again.