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