I was recently wanting to do an Extract Superclass refactoring in a Ruby on Rails solution which uses MongoDB and the glorious MongoMapper. Being somewhat new to MongoDB and MongoMapper, I was a bit scared about this refactoring. I also had collections full of data that I didn't want to screw up. I knew it would require massaging those documents, and also playing to the way MongoMapper handled inheritance.
Fortunately, it was a piece of cake.
Step 1: Change Ruby Classes
Step one was to do the Extract super class in the Ruby MongoMapper classes. Starting with two separate classes:
class Dog
include MongoMapper::Document
key :name, String
key :bones, Integer
end
class Cat
include MongoMapper::Document
key :name
key :fur_balls, Integer
end
...you can extract the superclass to give this:
class Animal
include MongoMapper::Document
key :name, String
end
class Dog < Animal
key :bones, Integer
end
class Cat < Animal
key :fur_balls, Integer
end
Step 2: Update Mongo DB
Now, what about Mongo? If you're in development and can drop/re-create your database then great, just run your boot script. But what about if you need to keep the existing stuff? You need to somehow merge the collections into a single hierarchy. It turns out to be pretty easy.
Firstly, logging into the mongo shell and looking at the collections will give this
> mongo -ublah -pblah some.server.com:27085/my-db
> show collections
cats
dogs
So, first off we need to create a collection for Animals.
> db.createCollection("animals")
Now, since Cat and Dog are a subclass of animals, we need to move them into the animals collection, and also let MongoMapper know that they are a subclass. This is done like so in your mongodb shell.
var cursor = dogs.find();
while(cursor.hasNext()){
var doc = cursor.next();
doc["_type"] = "Dog";
db.animals.insert(doc);
}
var cursor = cats.find();
while(cursor.hasNext()){
var doc = cursor.next();
doc["_type"] = "Cat";
db.animals.insert(doc);
}
Basically, this just loops through the dogs and cats adding them to the animals collection. But, we also add the type
field needed by MongoMapper so it knows what Ruby class to hydrate the MongoDB JSON structure into.
Step 3: Done!
You should probably tidy up and drop your old "cat" and "dog" collections too.
> db.dogs.drop()
> db.cats.drop()
TaDa! We're done.