You probably want has many through

has_and_belongs_to_many and has_many :through are two choices when implementing a many to many relationships with Active Record. They both requires a join table to work.

has_and_belongs_to_many automatically expects a join table to be named in a convention (you remember what’s the convention?1). While has_many :through you can name your join table.

Take famous example from twitter, A following relationship. How do we approach that? Here is what I would suggest:

class FollowingRelationship < ApplicationRecord
  belongs_to :follower,
    class_name: "User",
    counter_cache: :followed_users_count
  belongs_to :followed_user,
    class_name: "User",
    counter_cache: :followers_count

create_table "following_relationships" do |t|
  t.bigint "follower_id", null: false
  t.bigint "followed_user_id", null: false
  t.index ["followed_user_id"], name: "index_following_relationships_on_followed_user_id"
  t.index ["follower_id"], name: "index_following_relationships_on_follower_id"

class User < ApplicationRecord
  has_many :followed_user_relationships,
    foreign_key: :follower_id,
    class_name: "FollowingRelationship",
    dependent: :destroy
  has_many :followed_users,
    through: :followed_user_relationships

  has_many :follower_relationships,
    foreign_key: :followed_user_id,
    class_name: "FollowingRelationship",
    dependent: :destroy
  has_many :followers,
    through: :follower_relationships

create_table "users" do |t|
  t.integer "followed_users_count", default: 0, null: false
  t.integer "followers_count", default: 0, null: false

add_foreign_key "following_relationships", "users", column: "followed_user_id"
add_foreign_key "following_relationships", "users", column: "follower_id"

It takes additional setup compare to has_and_belongs_to_many but what happens when the business needs arrive that you need to extend the join table?

Another reasons is good design always encourages use many more individual objects to compose what we want to do. We want to put the logic at where it belongs.

I also seen a lot of people migrating from has_and_belongs_to_many to has_many :through, but your mileage may vary!

  • 1

    has_and_belongs_to_many :users on Tweet model will expect a join table named tweets_users because t comes before u. Source