Database

How to Migrate Mongoid to PostgreSQL in Rails

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.

Contact us for a Technical Debt Audit