Skip to content

Instantly share code, notes, and snippets.

@mehdi-farsi
Last active February 5, 2023 10:32
Show Gist options
  • Save mehdi-farsi/b98616585b03b7494f9179f3bbe4330d to your computer and use it in GitHub Desktop.
Save mehdi-farsi/b98616585b03b7494f9179f3bbe4330d to your computer and use it in GitHub Desktop.
Replace HABTM by HMT with Join Table as Model

Has And Belongs To Many (use case)

how it works ?

Let's take the simple example of a blog with:

  • A post habtm tags
  • A tag habtm posts

how is it represented in rails ?

Let's assume that a Post and Tag model already exist. This is the migration for the join table

class CreatePostsTags < ActiveRecord::Migration[5.1]
  def change
    create_table :posts_tags, id: false do |t|
      t.belongs_to :post, index: true
      t.belongs_to :tag, index: true
    end
  end
end

And the relations in each model

class Post < ApplicationRecord
  has_and_belongs_to_many :tags
end
class Tag < ApplicationRecord
  has_and_belongs_to_many :posts
end

Cool! it works !

Few months later, let's assume that you want to order tags in each post for searching purposes.

HABTM to has_many :through

why ?

In our case, has_many :through permits to declare the join table as model. And so, in order to operate (filter, sort, allow, reject, etc..) at a join table level. That permits to store the rank of the tag in the post's tag list direclty in the join model.

Migration issue

class CreatePostsTags < ActiveRecord::Migration[5.1]
  def change
    create_table :posts_tags, id: false do |t|
      t.belongs_to :post, index: true
      t.belongs_to :tag, index: true
    end
  end
end

A model needs a primary key to be retrieved. By convention rails uses the id column as primary key. The problem is that we've removed the id column from our join table (id: false). this makes sense because we just need the post_id and tag_id to retrieve a record.

But in order to use has_many :through, we need to add a primary key to the posts_tags table. So how to do so ?

class AddPrimaryKeyAndRankToPostsTags < ActiveRecord::Migration[5.1]
  def change
    rename_table 'posts_tags', 'post_tags'
    add_column :post_tags, :id, :primary_key
    add_column :post_tags, :rank, :integer, default: 0
  end
end

Then in models

class PostTag < ApplicationRecord
  belongs_to :post
  belongs_to :tag
end
class Post < ApplicationRecord
  has_many :post_tags, -> { order(rank: :asc) }
  has_many :tags, through: :post_tags
end
class Tag < ApplicationRecord
  has_many :post_tags
  has_many :posts, through: :post_tags
end

Voilà !

@mehdi-farsi
Copy link
Author

Thank you for the kind words!

Hope that helps!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment