I have a model A,
Class A < ActiveRecord::Base
has_many: names, class_name: 'B'
and a model B
class B < ActiveRecord::Base
belongs to :A
and there are already a bunch of data in database.
How do I write a migration to migrate them from one-to-many to many-to-many relationship? I prefer to use
has_many: through
if possible.
It's not hard to write the db migration, but what do I do to migrate the data in it?
This scenario comes up quite often in Rails projects and I'm surprised there still aren't a lot of how-tos out there as its a straightforward data evolution but requires some delicacy when dealing with already deployed systems.
I'm not sure if you're interested in polymorphic behavior for the many-to-many but I'm throwing that in as I find it useful for many many-to-many scenarios (pun intended! :-).
I had this before I started:
class Tag < ActiveRecord::Base
has_many :posts, inverse_of: :tag
class Post < ActiveRecord::Base
belongs_to :tag, inverse_of: :posts
I know, I know, why only one Tag for a Post? Turns out, I wanted my Posts to have multiple Tags after all. And then I thought, wait a minute, I want other things to have Tags as well, such as some kind of Thing.
You could use :has_and_belongs_to_many for each of Posts-Tags and Things-Tags but then that makes for 2 join tables and we'll probably want to Tag more entities as they get added right? The has_many :through
is a great option here for one side of our associations and avoids having multiple join tables.
We are going to do this in 2 STEPS involving 2 deploys:
Step 1 - No change to existing associations. A new Taggable model/migration that will be polymorphic with respect to Posts and Things. DEPLOY.
Step 2 - Update associations. New migration to drop the old :tag_id
foreign_key from Posts. DEPLOY.
The two steps are necessary to be able to execute your migration in Step 1 using your previous association definitions, otherwise your new associations will not work.
I think two steps is the simplest approach, recommended if your traffic is low enough that the risk of additional Tags being created on Posts/Things in between the two steps is low enough. If your traffic is very high, you can combine these two steps into one but you'll need to use different association names and then go back to delete the old unused ones after a working rollout. I'll leave the 1 step approach as an exercise for the reader :-)
Create a model migration for a new polymorphic join table.
rails g model Taggable tag_id:integer tagged_id:integer tagged_type:string --timestamps=false
Edit the resulting migration to revert to using #up
and #down
(instead of #change
) and add the data migration:
class CreateTaggables < ActiveRecord::Migration
def up
create_table :taggables do |t|
t.integer :tag_id
t.integer :tagged_id
t.string :tagged_type
end
# we pull Posts here as they have the foreign_key to tags...
Posts.all.each do |p|
Taggable.create(tag_id: p.tag_id, tagged_id: p.id, tagged_type: "Post")
end
end
def down
drop_table :taggables
end
end
Edit your new model:
class Taggable < ActiveRecord::Base
belongs_to :tag
belongs_to :tagged, polymorphic: true
end
At this point, DEPLOY your new model and migration. Great.
Now we're going to update our class definitions:
class Tag < ActiveRecord::Base
has_many :taggables
has_many :posts, through: :taggables, source: :tagged, source_type: "Post"
has_many :things, through: :taggables, source: :tagged, source_type: "Thing"
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags, join_table: 'taggables', foreign_key: :tagged_id
class Thing < ActiveRecord::Base
has_and_belongs_to_many :tags, join_table: 'taggables', foreign_key: :tagged_id
You should be able to add dependent: :destroy
on has_many :posts
and has_many :things
as :tag
is a belongs_to
on Taggable.
Don't forget to drop your old foreign_key:
class RemoveTagIdFromPosts < ActiveRecord::Migration
def up
remove_column :posts, :tag_id
end
def down
add_column :posts, :tag_id, :integer
end
end
Update your specs!
DEPLOY!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With