BLUF (Bottom Line Up Front): Legacy applications built on MongoDB (via Mongoid) often suffer from missing relational integrity and complex aggregations. Migrating to PostgreSQL is the modern standard. You can safely execute this by extracting document-oriented data via an ETL script, mapping predictable schemas to relational columns, and storing schemaless attributes in PostgreSQL JSONB columns.
Phase 1: The NoSQL Technical Debt
In 2013-2015, many Rails apps adopted MongoDB unnecessarily. As applications matured, the lack of JOIN operations and ACID compliance created severe data inconsistency and massive application-level logic to simulate relations.
Synthetic Engineering Context: The Complex Join
To display a list of Users with their latest Orders, a Mongoid application often triggers massive N+1 issues or requires complex MapReduce scripts.
# Legacy Mongoid Data Structure
class User
include Mongoid::Document
field :email, type: String
field :metadata, type: Hash # Schemaless data
has_many :orders
end
Migrating this to ActiveRecord requires normalizing the rigid data while preserving the flexibility of the metadata hash.
Phase 2: The ETL Migration Strategy
The migration requires a dual-database setup and a robust Extract, Transform, Load (ETL) script to move data from MongoDB to PostgreSQL.
Execution: Mapping Models
Configure your legacy Mongoid models and create new ActiveRecord models targeting Postgres.
# New ActiveRecord Model (Postgres)
class SqlUser < ApplicationRecord
self.table_name = "users"
# Ensure the JSONB column is utilized properly
store_accessor :metadata, :preferences, :last_login_ip
end
The PostgreSQL migration should utilize JSONB for the dynamic fields:
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :email, null: false
t.jsonb :metadata, default: {}, null: false
t.timestamps
end
add_index :users, :metadata, using: :gin
end
end
Execution: The Extraction Script
Use find_in_batches on the Mongoid side to prevent RAM exhaustion, and insert_all on the ActiveRecord side for high-performance bulk inserts.
# lib/tasks/mongo_to_pg.rake
namespace :migrate do
task mongo_to_pg: :environment do
batch_size = 1000
# Process Mongoid documents in batches
User.includes(:orders).each_slice(batch_size) do |mongo_users|
sql_users = mongo_users.map do |mu|
{
id: mu.id.to_s, # Preserve string IDs or map to UUIDs
email: mu.email,
metadata: mu.metadata.to_json, # Convert Hash to JSON string for Postgres JSONB
created_at: mu.created_at,
updated_at: mu.updated_at
}
end
# Bulk insert into Postgres
SqlUser.insert_all(sql_users)
end
puts "Migration Complete."
end
end
Phase 3: Next Steps & Risk Mitigation
This ETL process works for static data, but live applications require zero-downtime dual-writing strategies during the cutover. Switching ORMs from Mongoid to ActiveRecord also requires rewriting all querying logic across your controllers.
Need Help Stabilizing Your Legacy App? Migrating off MongoDB is a massive architectural shift. Our team at USEO has proven experience moving complex, terabyte-scale NoSQL data into optimized PostgreSQL architectures.