This is a not that common of a problem, though there are other posts on the topic. But my solution’s a little different from what I found, so I figured it was blog-worthy. I used this in a rails 3.0 app. I’ve also only tried it with sqlite, so it may totally blow up when I push to heroku. YMMV.

Take, for example, some initial tables:

users – id:primary_key
replies – id:primary_key
liked_replies – user_id:integer, reply_id:integer

and some initial models:

class User < ActiveRecord::Base
  has_and_belongs_to_many :liked_replies, :class_name => 'Reply', :join_table => 'liked_replies'
end

class Reply < ActiveRecord::Base
  has_and_belongs_to_many :likers, :class_name => 'User', :join_table => 'liked_replies'
end

And, of course, there’s existing data that you don’t want to destroy.

So, I generated a migration:

bundle exec rails g migration add_id_to_liked_replies id:primary_key

If you’re curious, the migration looks like this:

class AddIdToLikedReplies < ActiveRecord::Migration
  def self.up
    add_column :liked_replies, :id, :primary_key
  end
  def self.down
    remove_column :liked_replies, :id
  end
end

Migrate as usual, and then the app stops running, because routes can’t be built, because User is a devise model, and devise_for tries to load User which has an association that generates one of these:

Primary key is not allowed in a has_and_belongs_to_many join table (liked_replies). (ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError)

Hm. This will probably happen when we deploy, too. And this makes a db:rollback fail, too. !!!! So, how can we make the code work with or without a primary key? I’ve seen lots of people talk about making your code work with the before & after version of your db, and that seems like the right goal here, too.

Here are my new model classes that work with either the new or old liked_replies table:

class User < ActiveRecord::Base
  begin
    has_and_belongs_to_many :liked_replies, :class_name => 'Reply', :join_table => 'liked_replies'
  rescue
    has_many :liked_reply_records, :class_name => 'LikedReply', :dependent => :destroy
    has_many :liked_replies, :through => :liked_reply_records, :source => :reply
  end
end

class Reply < ActiveRecord::Base
  begin
    has_and_belongs_to_many :likers, :class_name => 'User', :join_table => 'liked_replies'
  rescue
    has_many :liked_replies
    has_many :likers, :through => :liked_replies, :source => :user
  end
end

class LikedReply < ActiveRecord::Base
  belongs_to :user
  belongs_to :reply
end