Mongoid
Mongoid is "the officially supported object-document mapper (ODM) for MongoDB in Ruby" developed as a comprehensive ODM framework that provides seamless integration between Ruby applications and MongoDB. With API parity to ActiveRecord wherever possible, it ensures developers working with Rails applications can use familiar methods and mechanics. Mongoid supports flexible data modeling native to Ruby applications, offering powerful features like embedded documents, dynamic attributes, complex queries, and comprehensive validation. As the standard MongoDB solution for Ruby development, it provides production-ready performance and maintainability for modern web applications.
GitHub Overview
mongodb/mongoid
The Official Ruby Object Mapper for MongoDB
Topics
Star History
Library
Mongoid
Overview
Mongoid is "the officially supported object-document mapper (ODM) for MongoDB in Ruby" developed as a comprehensive ODM framework that provides seamless integration between Ruby applications and MongoDB. With API parity to ActiveRecord wherever possible, it ensures developers working with Rails applications can use familiar methods and mechanics. Mongoid supports flexible data modeling native to Ruby applications, offering powerful features like embedded documents, dynamic attributes, complex queries, and comprehensive validation. As the standard MongoDB solution for Ruby development, it provides production-ready performance and maintainability for modern web applications.
Details
Mongoid 2025 edition fully supports Ruby 3.3 and Rails 8, providing a mature ODM experience that maximizes modern Ruby features including pattern matching, fiber schedulers, and type annotations. The beautiful DSL allows intuitive definition of document models with embedded relationships, inheritance hierarchies, and custom field types. Deep Rails integration includes automatic configuration, ActiveJob compatibility, and seamless controller patterns. MongoDB's document nature enables flexible schema evolution, powerful aggregation pipelines, and efficient geospatial queries, making it ideal for modern applications requiring rapid development and horizontal scaling.
Key Features
- Document-oriented Design: Natural mapping between Ruby objects and MongoDB documents
- Embedded Documents: Hierarchical data structures with nested relationships
- Dynamic Attributes: Flexible schema with runtime field addition
- Rails Integration: Seamless ActiveRecord-like API for Rails developers
- Advanced Querying: MongoDB aggregation pipelines and complex criteria
- Geospatial Support: Location-based queries and indexing
Pros and Cons
Pros
- ActiveRecord-like API provides familiar development experience for Rails developers
- Perfect integration with Rails ecosystem and convention-over-configuration
- Flexible schema allows rapid prototyping and evolution
- Excellent performance for read-heavy workloads and document-oriented data
- Built-in support for embedded documents and complex nested structures
- Comprehensive documentation and active community support
Cons
- Learning curve for developers accustomed to relational database concepts
- MongoDB dependency limits deployment options compared to SQL databases
- Complex queries may require understanding of MongoDB aggregation framework
- Memory usage can be high with large embedded documents
- Transactions support is limited compared to traditional RDBMS
- Index management requires careful planning for optimal performance
Reference Pages
Code Examples
Setup
# Gemfile
gem 'mongoid', '~> 9.0'
gem 'mongo', '~> 2.21'
# Install gems
bundle install
# config/mongoid.yml
development:
clients:
default:
database: blog_development
hosts:
- localhost:27017
options:
read:
mode: :primary
max_pool_size: 1
test:
clients:
default:
database: blog_test
hosts:
- localhost:27017
options:
read:
mode: :primary
max_pool_size: 1
production:
clients:
default:
uri: <%= ENV['MONGODB_URI'] %>
options:
max_pool_size: 20
retry_writes: true
w: :majority
# config/application.rb (Rails)
require 'mongoid'
Mongoid.load!(Rails.root.join('config', 'mongoid.yml'), Rails.env)
# Standalone usage
require 'mongoid'
Mongoid.configure do |config|
config.connect_to("blog_development")
end
Basic Model Definition
# app/models/user.rb
class User
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Attributes::Dynamic # For dynamic fields
# Basic fields
field :name, type: String
field :email, type: String
field :age, type: Integer, default: 18
field :is_active, type: Boolean, default: true
field :tags, type: Array, default: []
field :metadata, type: Hash, default: {}
field :birth_date, type: Date
field :last_login_at, type: DateTime
# Validations
validates :name, presence: true, length: { minimum: 2, maximum: 50 }
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :age, numericality: { greater_than: 0, less_than: 150 }
# Indexes
index({ email: 1 }, { unique: true })
index({ name: 1, age: 1 })
index({ is_active: 1, last_login_at: -1 })
index({ tags: 1 })
# Callbacks
before_save :normalize_email
after_create :send_welcome_email
# Scopes
scope :active, -> { where(is_active: true) }
scope :adults, -> { where(:age.gte => 18) }
scope :recent_login, -> { where(:last_login_at.gte => 1.week.ago) }
# Instance methods
def full_name
name.titleize
end
def adult?
age >= 18
end
def activate!
update!(is_active: true, last_login_at: Time.current)
end
def deactivate!
update!(is_active: false)
end
private
def normalize_email
self.email = email.downcase.strip if email.present?
end
def send_welcome_email
# Welcome email logic
end
end
# app/models/address.rb - Embedded document
class Address
include Mongoid::Document
field :street, type: String
field :city, type: String
field :state, type: String
field :postal_code, type: String
field :country, type: String, default: 'USA'
field :coordinates, type: Array # [longitude, latitude]
validates :street, :city, :postal_code, presence: true
validates :postal_code, format: { with: /\A\d{5}(-\d{4})?\z/ }
# Geospatial index
index({ coordinates: '2dsphere' })
embedded_in :user
def full_address
"#{street}, #{city}, #{state} #{postal_code}, #{country}"
end
end
# Add embedded relationship to User
class User
embeds_one :address
accepts_nested_attributes_for :address
end
Relationships
# app/models/post.rb
class Post
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Slug
field :title, type: String
field :content, type: String
field :excerpt, type: String
field :is_published, type: Boolean, default: false
field :published_at, type: DateTime
field :view_count, type: Integer, default: 0
field :tags, type: Array, default: []
field :featured_image_url, type: String
# Slug generation
slug :title
# Relationships
belongs_to :user
belongs_to :category
has_many :comments, dependent: :destroy
has_and_belongs_to_many :topics
# Validations
validates :title, presence: true, length: { maximum: 200 }
validates :content, presence: true, length: { minimum: 10 }
validates :user, presence: true
# Indexes
index({ user_id: 1, published_at: -1 })
index({ is_published: 1, published_at: -1 })
index({ tags: 1 })
index({ _slugs: 1 }, { unique: true })
# Scopes
scope :published, -> { where(is_published: true) }
scope :recent, -> { order(published_at: :desc) }
scope :by_tag, ->(tag) { where(tags: tag) }
scope :popular, -> { order(view_count: :desc) }
# Callbacks
before_save :generate_excerpt
before_save :set_published_at
def publish!
update!(is_published: true, published_at: Time.current)
end
def unpublish!
update!(is_published: false)
end
def increment_view_count!
inc(view_count: 1)
end
def related_posts(limit = 5)
Post.published
.where(:_id.ne => id)
.any_of(
{ tags: { '$in' => tags } },
{ category_id: category_id }
)
.limit(limit)
end
private
def generate_excerpt
if content.present? && excerpt.blank?
self.excerpt = content.truncate(200)
end
end
def set_published_at
if is_published? && published_at.blank?
self.published_at = Time.current
end
end
end
# app/models/category.rb
class Category
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Tree
field :name, type: String
field :description, type: String
field :color, type: String, default: '#007bff'
field :is_active, type: Boolean, default: true
has_many :posts
has_many :subcategories, class_name: 'Category', inverse_of: :parent
belongs_to :parent, class_name: 'Category', optional: true
validates :name, presence: true, uniqueness: true
validates :color, format: { with: /\A#[0-9A-Fa-f]{6}\z/ }
index({ name: 1 }, { unique: true })
index({ parent_id: 1 })
scope :active, -> { where(is_active: true) }
scope :root_categories, -> { where(parent_id: nil) }
def posts_count
posts.count
end
def total_posts_count
posts.count + subcategories.sum(&:total_posts_count)
end
end
# app/models/comment.rb
class Comment
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Tree
field :content, type: String
field :author_name, type: String
field :author_email, type: String
field :is_approved, type: Boolean, default: false
field :is_spam, type: Boolean, default: false
field :user_agent, type: String
field :ip_address, type: String
belongs_to :post
belongs_to :user, optional: true # For registered users
has_many :replies, class_name: 'Comment', inverse_of: :parent
belongs_to :parent, class_name: 'Comment', optional: true
validates :content, presence: true, length: { minimum: 5, maximum: 1000 }
validates :author_name, presence: true, unless: :user_id?
validates :author_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }, unless: :user_id?
index({ post_id: 1, created_at: -1 })
index({ is_approved: 1, created_at: -1 })
index({ parent_id: 1 })
scope :approved, -> { where(is_approved: true) }
scope :pending, -> { where(is_approved: false) }
scope :top_level, -> { where(parent_id: nil) }
def approve!
update!(is_approved: true)
end
def mark_as_spam!
update!(is_spam: true, is_approved: false)
end
def author_display_name
user&.name || author_name
end
end
# app/models/topic.rb
class Topic
include Mongoid::Document
include Mongoid::Timestamps
field :name, type: String
field :description, type: String
field :posts_count, type: Integer, default: 0
has_and_belongs_to_many :posts
validates :name, presence: true, uniqueness: true
index({ name: 1 }, { unique: true })
def increment_posts_count!
inc(posts_count: 1)
end
def decrement_posts_count!
inc(posts_count: -1) if posts_count > 0
end
end
Querying and Aggregation
# app/services/user_service.rb
class UserService
# Basic CRUD operations
def self.create_user(params)
User.create!(params)
end
def self.find_user(id)
User.find(id)
rescue Mongoid::Errors::DocumentNotFound
nil
end
def self.update_user(id, params)
user = User.find(id)
user.update!(params)
user
end
def self.delete_user(id)
user = User.find(id)
user.destroy
end
# Advanced queries
def self.search_users(query, filters = {})
criteria = User.all
# Text search
if query.present?
criteria = criteria.any_of(
{ name: /#{Regexp.escape(query)}/i },
{ email: /#{Regexp.escape(query)}/i }
)
end
# Age filter
if filters[:min_age].present?
criteria = criteria.where(:age.gte => filters[:min_age])
end
if filters[:max_age].present?
criteria = criteria.where(:age.lte => filters[:max_age])
end
# Status filter
if filters[:is_active].present?
criteria = criteria.where(is_active: filters[:is_active])
end
# Tag filter
if filters[:tags].present?
criteria = criteria.where(tags: { '$in' => Array(filters[:tags]) })
end
# Date range filter
if filters[:created_after].present?
criteria = criteria.where(:created_at.gte => filters[:created_after])
end
criteria.order_by(filters[:sort_by]&.to_sym || :created_at => :desc)
.limit(filters[:limit] || 20)
.offset(filters[:offset] || 0)
end
def self.users_by_location(latitude, longitude, radius_km = 10)
User.where(
'address.coordinates' => {
'$near' => {
'$geometry' => {
type: 'Point',
coordinates: [longitude, latitude]
},
'$maxDistance' => radius_km * 1000 # Convert to meters
}
}
)
end
def self.user_statistics
pipeline = [
{
'$group' => {
'_id' => nil,
'total_users' => { '$sum' => 1 },
'active_users' => {
'$sum' => { '$cond' => [{ '$eq' => ['$is_active', true] }, 1, 0] }
},
'average_age' => { '$avg' => '$age' },
'age_distribution' => {
'$push' => {
'$switch' => {
'branches' => [
{ 'case' => { '$lt' => ['$age', 18] }, 'then' => 'under_18' },
{ 'case' => { '$lt' => ['$age', 30] }, 'then' => '18_29' },
{ 'case' => { '$lt' => ['$age', 50] }, 'then' => '30_49' },
{ 'case' => { '$gte' => ['$age', 50] }, 'then' => '50_plus' }
],
'default' => 'unknown'
}
}
}
}
}
]
User.collection.aggregate(pipeline).first
end
end
# app/services/blog_service.rb
class BlogService
def self.published_posts_with_stats
Post.collection.aggregate([
{ '$match' => { 'is_published' => true } },
{
'$lookup' => {
'from' => 'users',
'localField' => 'user_id',
'foreignField' => '_id',
'as' => 'author'
}
},
{
'$lookup' => {
'from' => 'comments',
'localField' => '_id',
'foreignField' => 'post_id',
'as' => 'comments'
}
},
{
'$project' => {
'title' => 1,
'published_at' => 1,
'view_count' => 1,
'author_name' => { '$arrayElemAt' => ['$author.name', 0] },
'comments_count' => { '$size' => '$comments' },
'approved_comments_count' => {
'$size' => {
'$filter' => {
'input' => '$comments',
'cond' => { '$eq' => ['$$this.is_approved', true] }
}
}
}
}
},
{ '$sort' => { 'published_at' => -1 } },
{ '$limit' => 10 }
])
end
def self.popular_tags(limit = 10)
Post.collection.aggregate([
{ '$match' => { 'is_published' => true } },
{ '$unwind' => '$tags' },
{ '$group' => { '_id' => '$tags', 'count' => { '$sum' => 1 } } },
{ '$sort' => { 'count' => -1 } },
{ '$limit' => limit }
])
end
def self.monthly_post_stats(year = Date.current.year)
Post.collection.aggregate([
{
'$match' => {
'is_published' => true,
'published_at' => {
'$gte' => Time.new(year, 1, 1),
'$lt' => Time.new(year + 1, 1, 1)
}
}
},
{
'$group' => {
'_id' => { '$month' => '$published_at' },
'count' => { '$sum' => 1 },
'total_views' => { '$sum' => '$view_count' }
}
},
{ '$sort' => { '_id' => 1 } }
])
end
end
Embedded Documents and Complex Operations
# app/models/profile.rb
class Profile
include Mongoid::Document
field :bio, type: String
field :website, type: String
field :social_links, type: Hash, default: {}
field :skills, type: Array, default: []
field :experience_years, type: Integer
field :location, type: String
field :avatar_url, type: String
field :preferences, type: Hash, default: {}
embeds_many :work_experiences
embeds_many :educations
embedded_in :user
validates :bio, length: { maximum: 500 }
validates :website, format: { with: URI::DEFAULT_PARSER.make_regexp }, allow_blank: true
end
# app/models/work_experience.rb
class WorkExperience
include Mongoid::Document
field :company, type: String
field :position, type: String
field :description, type: String
field :start_date, type: Date
field :end_date, type: Date
field :is_current, type: Boolean, default: false
field :skills_used, type: Array, default: []
embedded_in :profile
validates :company, :position, :start_date, presence: true
validate :end_date_after_start_date
scope :current, -> { where(is_current: true) }
scope :past, -> { where(is_current: false) }
def duration_in_months
end_date_for_calculation = is_current? ? Date.current : end_date
return 0 unless end_date_for_calculation
((end_date_for_calculation.year * 12 + end_date_for_calculation.month) -
(start_date.year * 12 + start_date.month))
end
private
def end_date_after_start_date
if end_date.present? && end_date < start_date
errors.add(:end_date, 'must be after start date')
end
end
end
# app/models/education.rb
class Education
include Mongoid::Document
field :institution, type: String
field :degree, type: String
field :field_of_study, type: String
field :start_year, type: Integer
field :end_year, type: Integer
field :gpa, type: Float
field :description, type: String
embedded_in :profile
validates :institution, :degree, :start_year, presence: true
validates :gpa, numericality: { greater_than: 0, less_than_or_equal_to: 4.0 }, allow_nil: true
validate :end_year_after_start_year
private
def end_year_after_start_year
if end_year.present? && end_year < start_year
errors.add(:end_year, 'must be after start year')
end
end
end
# Update User model to include profile
class User
embeds_one :profile
accepts_nested_attributes_for :profile
def complete_profile?
profile.present? &&
profile.bio.present? &&
profile.work_experiences.any? &&
profile.educations.any?
end
end
# Service for complex operations
class UserProfileService
def self.create_complete_profile(user_params, profile_params)
User.transaction do
user = User.create!(user_params)
profile = user.build_profile(profile_params.except(:work_experiences, :educations))
# Add work experiences
profile_params[:work_experiences]&.each do |exp_params|
profile.work_experiences.build(exp_params)
end
# Add educations
profile_params[:educations]&.each do |edu_params|
profile.educations.build(edu_params)
end
user.save!
user
end
end
def self.update_profile_with_experience(user_id, experience_params)
user = User.find(user_id)
user.profile ||= user.build_profile
experience = user.profile.work_experiences.build(experience_params)
user.save!
# Update total experience years
total_months = user.profile.work_experiences.sum(&:duration_in_months)
user.profile.update!(experience_years: (total_months / 12.0).round(1))
user
end
def self.find_users_by_skills(skills, location = nil)
criteria = User.where('profile.skills' => { '$in' => Array(skills) })
criteria = criteria.where('profile.location' => /#{Regexp.escape(location)}/i) if location.present?
criteria.includes(:profile)
.order_by('profile.experience_years' => :desc)
end
end
Error Handling and Validation
# app/services/robust_user_service.rb
class RobustUserService
class UserCreationError < StandardError; end
class UserNotFoundError < StandardError; end
class ValidationError < StandardError; end
def self.create_user_safely(params)
begin
user = User.new(params)
if user.valid?
user.save!
{ success: true, user: user, message: 'User created successfully' }
else
{ success: false, errors: user.errors.full_messages }
end
rescue Mongo::Error::OperationFailure => e
if e.message.include?('duplicate key error')
{ success: false, errors: ['Email already exists'] }
else
Rails.logger.error "MongoDB error: #{e.message}"
{ success: false, errors: ['Database error occurred'] }
end
rescue => e
Rails.logger.error "Unexpected error: #{e.message}"
{ success: false, errors: ['System error occurred'] }
end
end
def self.update_user_safely(id, params)
begin
user = User.find(id)
if user.update(params)
{ success: true, user: user, message: 'User updated successfully' }
else
{ success: false, errors: user.errors.full_messages }
end
rescue Mongoid::Errors::DocumentNotFound
{ success: false, errors: ['User not found'] }
rescue Mongoid::Errors::Validations => e
{ success: false, errors: e.document.errors.full_messages }
rescue => e
Rails.logger.error "Error updating user: #{e.message}"
{ success: false, errors: ['Update failed'] }
end
end
def self.bulk_update_users(user_ids, updates)
results = { success: [], failed: [], total: user_ids.count }
user_ids.each do |user_id|
result = update_user_safely(user_id, updates)
if result[:success]
results[:success] << { id: user_id, user: result[:user] }
else
results[:failed] << { id: user_id, errors: result[:errors] }
end
end
results
end
def self.data_integrity_check
issues = []
# Check for users without email
users_without_email = User.where(email: nil).count
if users_without_email > 0
issues << "#{users_without_email} users found without email addresses"
end
# Check for invalid age values
invalid_ages = User.where(:age.lt => 0).or(:age.gt => 150).count
if invalid_ages > 0
issues << "#{invalid_ages} users found with invalid age values"
end
# Check for orphaned posts
orphaned_posts = Post.where(:user_id.nin => User.pluck(:id)).count
if orphaned_posts > 0
issues << "#{orphaned_posts} posts found without valid user references"
end
# Check for broken embedded documents
broken_profiles = User.where('profile.bio' => nil).where(:profile.exists => true).count
if broken_profiles > 0
issues << "#{broken_profiles} users found with incomplete profile data"
end
{
healthy: issues.empty?,
issues: issues,
checked_at: Time.current
}
end
def self.cleanup_orphaned_data
cleanup_results = {}
# Remove posts without valid users
orphaned_posts_count = Post.where(:user_id.nin => User.pluck(:id)).count
Post.where(:user_id.nin => User.pluck(:id)).destroy_all
cleanup_results[:orphaned_posts_removed] = orphaned_posts_count
# Remove comments without valid posts
orphaned_comments_count = Comment.where(:post_id.nin => Post.pluck(:id)).count
Comment.where(:post_id.nin => Post.pluck(:id)).destroy_all
cleanup_results[:orphaned_comments_removed] = orphaned_comments_count
# Clean up invalid embedded documents
users_with_broken_profiles = User.where('profile.bio' => nil).where(:profile.exists => true)
users_with_broken_profiles.each { |user| user.unset(:profile) }
cleanup_results[:broken_profiles_cleaned] = users_with_broken_profiles.count
cleanup_results[:cleaned_at] = Time.current
cleanup_results
end
end
# Usage examples
def demonstrate_error_handling
# Safe user creation
result = RobustUserService.create_user_safely({
name: 'Test User',
email: '[email protected]',
age: 25
})
if result[:success]
puts "User created: #{result[:user].name}"
else
puts "Creation failed: #{result[:errors].join(', ')}"
end
# Bulk operations
user_ids = User.limit(5).pluck(:id)
bulk_result = RobustUserService.bulk_update_users(user_ids, { is_active: false })
puts "Bulk update: #{bulk_result[:success].count} successful, #{bulk_result[:failed].count} failed"
# Data integrity check
integrity_result = RobustUserService.data_integrity_check
if integrity_result[:healthy]
puts "Data integrity check passed"
else
puts "Issues found: #{integrity_result[:issues].join(', ')}"
end
# Cleanup
cleanup_result = RobustUserService.cleanup_orphaned_data
puts "Cleanup completed: #{cleanup_result}"
end