Mongoid
Mongoidは「Ruby用のエレガントな MongoDB ODM」として開発された、Ruby on RailsのActiveRecordライクなAPIを提供するMongoDB用Object-Document Mapper。Railsとのシームレスな統合、MongoDBのネイティブ機能を活かした柔軟なデータモデリング、パフォーマンスを重視した設計が特徴。スキーマレスなMongoDBの特性を生かし、リレーション管理、バリデーション、クエリ最適化等豊富な機能を提供し、Railsアプリケーション開発に最適化されています。
GitHub概要
mongodb/mongoid
The Official Ruby Object Mapper for MongoDB
トピックス
スター履歴
ライブラリ
Mongoid
概要
Mongoidは「Ruby用のエレガントな MongoDB ODM」として開発された、Ruby on RailsのActiveRecordライクなAPIを提供するMongoDB用Object-Document Mapper。Railsとのシームレスな統合、MongoDBのネイティブ機能を活かした柔軟なデータモデリング、パフォーマンスを重視した設計が特徴。スキーマレスなMongoDBの特性を生かし、リレーション管理、バリデーション、クエリ最適化等豊富な機能を提供し、Railsアプリケーション開発に最適化されています。
詳細
Mongoid 2025年版はRubyエコシステムで最も信頼されるMongoDB ODMとしての地位を維持しています。ActiveRecordパターンを採用し、Rails開発者に馴染みのあるAPIを提供しながら、MongoDBのドキュメント指向、スキーマレス、水平スケーリング等の特性をフル活用。埋め込みドキュメント、配列フィールド、動的フィールドなどMongoDB固有の機能を簡単に扱えるモデル定義、Criteria APIによる表現力豊かなクエリ、コールバック、バリデーション等Railsの正規機能をサポート。MongoDB 6.xを含む最新バージョン対応、Rails 7.x系列との完全互換性、シャーディングやレプリカセット等高度な機能を提供します。
主な特徴
- ActiveRecordライクAPI: Rails開発者に馴染みのある直感的なAPI
- MongoDBネイティブ機能: 埋め込みドキュメント、配列、動的フィールド対応
- Criteria API: 柔軟で表現力豊かなクエリインターフェース
- Rails統合: シームレスなRailsアプリケーション統合
- スキーマ柔軟性: スキーマレスモデルと動的フィールド対応
- パフォーマンス最適化: インデックス、アグリゲーション、バッチ操作対応
メリット・デメリット
メリット
- Railsアプリケーションとのシームレスな統合と高い生産性
- ActiveRecordに慣れた開発者にとって学習コストが低い
- MongoDBのスキーマレス性を活かした柔軟なデータモデリング
- 埋め込みドキュメントと配列フィールドの強力なサポート
- 豊富なCriteria APIとMongoDB機能への直接アクセス
- 活発なコミュニティと充実したドキュメント
デメリット
- リレーショナルDBに比べJOIN操作やトランザクション機能が制限的
- MongoDBの限定的なトランザクションサポートに依存
- NoSQL数据库的特性によるデータ整合性の課題
- スキーマレス設計によるデータ品質管理の困難さ
- シャーディング環境での複雑なクエリ最適化が必要
- 大量データ処理時のメモリ使用量の注意が必要
参考ページ
書き方の例
インストールと基本設定
# Gemfile
gem 'mongoid', '~> 8.1'
gem 'bson_ext' # パフォーマンス向上のため推奨
# Railsアプリケーションの場合
# bundle install
# rails generate mongoid:config
# config/mongoid.yml (基本設定)
development:
clients:
default:
database: my_app_development
hosts:
- localhost:27017
options:
server_selection_timeout: 5
# コネクションプール設定
max_pool_size: 10
min_pool_size: 1
production:
clients:
default:
uri: <%= ENV['MONGODB_URI'] %>
options:
max_pool_size: 20
min_pool_size: 5
# SSL設定
ssl: true
ssl_verify: true
基本モデル定義とCRUD操作
require 'mongoid'
# 基本的なモデル定義
class User
include Mongoid::Document
include Mongoid::Timestamps # created_at, updated_atを自動追加
field :name, type: String
field :email, type: String
field :age, type: Integer
field :active, type: Boolean, default: true
field :tags, type: Array, default: []
field :profile, type: Hash, default: {}
# バリデーション
validates :name, presence: true, length: { minimum: 2 }
validates :email, presence: true, uniqueness: true, format: /@/
validates :age, numericality: { greater_than: 0, less_than: 150 }
# インデックス
index({ email: 1 }, { unique: true })
index({ name: 1, age: 1 })
index({ tags: 1 })
end
class Article
include Mongoid::Document
include Mongoid::Timestamps
field :title, type: String
field :content, type: String
field :published_at, type: Time
field :view_count, type: Integer, default: 0
field :tags, type: Array, default: []
# リレーションシップ
belongs_to :user
embeds_many :comments
validates :title, presence: true, length: { minimum: 5 }
validates :content, presence: true, length: { minimum: 10 }
# スコープ
scope :published, -> { where(:published_at.ne => nil) }
scope :recent, -> { order(created_at: :desc) }
scope :popular, -> { order(view_count: :desc) }
end
# 埋め込みドキュメント
class Comment
include Mongoid::Document
include Mongoid::Timestamps
field :content, type: String
field :author_name, type: String
field :approved, type: Boolean, default: false
embedded_in :article
validates :content, presence: true, length: { minimum: 3 }
validates :author_name, presence: true
end
# 基本的なCRUD操作
# 作成
user = User.create!(
name: '田中太郎',
email: '[email protected]',
age: 30,
tags: ['プログラマー', 'Ruby'],
profile: { city: '東京', hobby: 'コーディング' }
)
# 検索
user = User.find_by(email: '[email protected]')
users = User.where(age: 30..40)
active_users = User.where(active: true)
programmers = User.where(tags: 'プログラマー')
# 更新
user.update!(age: 31)
User.where(active: false).update_all(active: true)
# 削除
user.destroy
User.where(:created_at.lt => 1.year.ago).delete_all
高度なクエリとアグリゲーション
# Criteria APIを使った複雑なクエリ
class UserService
# 条件的検索
def self.search_users(filters = {})
criteria = User.all
criteria = criteria.where(name: /#{filters[:name]}/i) if filters[:name]
criteria = criteria.where(age: filters[:min_age]..filters[:max_age]) if filters[:min_age] && filters[:max_age]
criteria = criteria.where(tags: { '$in' => filters[:tags] }) if filters[:tags]&.any?
criteria = criteria.where(active: filters[:active]) unless filters[:active].nil?
criteria.order_by(filters[:sort_by] || :created_at => :desc)
end
# ジオスパーシャルクエリ(位置情報がある場合)
def self.users_near_location(latitude, longitude, max_distance = 1000)
User.where(
location: {
'$near' => {
'$geometry' => {
type: 'Point',
coordinates: [longitude, latitude]
},
'$maxDistance' => max_distance
}
}
)
end
# テキスト検索(インデックスが必要)
def self.search_by_text(query)
User.where('$text' => { '$search' => query })
end
end
# MongoDBアグリゲーションパイプライン
class AnalyticsService
def self.user_statistics_by_age
User.collection.aggregate([
{
'$group' => {
_id: {
'$floor' => {
'$divide' => ['$age', 10]
}
},
count: { '$sum' => 1 },
avg_age: { '$avg' => '$age' },
total_articles: {
'$sum' => {
'$size' => { '$ifNull' => ['$article_ids', []] }
}
}
}
},
{
'$sort' => { '_id' => 1 }
}
])
end
def self.popular_tags
User.collection.aggregate([
{ '$unwind' => '$tags' },
{
'$group' => {
_id: '$tags',
count: { '$sum' => 1 }
}
},
{ '$sort' => { count: -1 } },
{ '$limit' => 20 }
])
end
def self.monthly_user_growth
User.collection.aggregate([
{
'$group' => {
_id: {
year: { '$year' => '$created_at' },
month: { '$month' => '$created_at' }
},
count: { '$sum' => 1 }
}
},
{
'$sort' => {
'_id.year' => 1,
'_id.month' => 1
}
}
])
}
end
リレーションシップと埋め込み
# 複雑なリレーションシップの例
class User
include Mongoid::Document
# 1:N リレーション
has_many :articles, dependent: :destroy
has_many :comments, class_name: 'Comment', foreign_key: :user_id
# N:N リレーション
has_and_belongs_to_many :followers, class_name: 'User', inverse_of: :following
has_and_belongs_to_many :following, class_name: 'User', inverse_of: :followers
# 埋め込みドキュメント
embeds_one :profile
embeds_many :addresses
end
class Profile
include Mongoid::Document
field :bio, type: String
field :website, type: String
field :avatar_url, type: String
field :social_links, type: Hash, default: {}
embedded_in :user
end
class Address
include Mongoid::Document
field :label, type: String # '家', '会社'等
field :street, type: String
field :city, type: String
field :postal_code, type: String
field :country, type: String, default: '日本'
field :primary, type: Boolean, default: false
embedded_in :user
validates :label, presence: true
validates :street, presence: true
validates :city, presence: true
end
# リレーションを使った操作
user = User.create!(name: '佐藤花子', email: '[email protected]')
# プロフィール作成
user.create_profile(
bio: 'Ruby開発者です',
website: 'https://sato.dev',
social_links: {
twitter: '@sato_dev',
github: 'sato-hanako'
}
)
# 住所追加
user.addresses.create!(
label: '家',
street: '渋谷区渋谷123-45',
city: '東京都',
postal_code: '150-0002',
primary: true
)
# 記事作成
article = user.articles.create!(
title: 'Mongoidの使い方',
content: 'Mongoidは素晴らしいODMです...',
tags: ['Ruby', 'MongoDB', 'Mongoid']
)
# コメント追加
article.comments.create!(
content: 'とても参考になりました!',
author_name: '田中太郎'
)
# フォロー関係
follower = User.find_by(email: '[email protected]')
user.followers << follower
# リレーションを含むクエリ
User.includes(:articles, :profile).where(name: /佐藤/)
articles_with_users = Article.includes(:user).published
パフォーマンス最適化とバッチ操作
# インデックス最適化
class User
include Mongoid::Document
# 単一フィールドインデックス
index({ email: 1 }, { unique: true, background: true })
index({ created_at: -1 }, { background: true })
# 複合インデックス
index({ active: 1, age: 1 }, { background: true })
index({ tags: 1, created_at: -1 }, { background: true })
# テキストインデックス
index({ name: 'text', 'profile.bio' => 'text' }, { background: true })
# ジオスペーシャルインデックス
index({ location: '2dsphere' }, { background: true })
end
# バッチ操作とパフォーマンス最適化
class BulkOperationService
# バルクインサート
def self.bulk_create_users(user_data_array)
User.collection.insert_many(user_data_array)
end
# バルク更新
def self.bulk_update_user_status(user_ids, status)
User.where(:id.in => user_ids).update_all(active: status)
end
# 効率的なカウント
def self.count_users_efficiently
# 通常のcountは遅い可能性がある
# User.count
# estimated_document_countは高速(正確性は低い)
User.collection.estimated_document_count
end
# メモリ効率的なバッチ処理
def self.process_users_in_batches(batch_size = 1000)
User.batch_size(batch_size).no_timeout.each do |user|
# ユーザーごとの処理
process_user(user)
end
end
# カーソルでの大量データ処理
def self.export_users_efficiently
cursor = User.collection.find({}, { batch_size: 1000 })
cursor.each do |user_doc|
# ユーザードキュメントごとの処理
export_user_data(user_doc)
end
end
private
def self.process_user(user)
# 実際の処理ロジック
puts "Processing user: #{user.name}"
end
def self.export_user_data(user_doc)
# エクスポート処理
puts "Exporting: #{user_doc['name']}"
end
end
テスト環境での利用
# RSpecでのテスト設定
# spec/spec_helper.rb
require 'mongoid'
require 'database_cleaner/mongoid'
Mongoid.load!('config/mongoid.yml', :test)
RSpec.configure do |config|
config.include Mongoid::Matchers, type: :model
config.before(:suite) do
DatabaseCleaner[:mongoid].strategy = :deletion
DatabaseCleaner[:mongoid].clean_with(:deletion)
end
config.around(:each) do |example|
DatabaseCleaner[:mongoid].cleaning do
example.run
end
end
end
# モデルテストの例
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
describe 'validations' do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
it { should validate_length_of(:name).with_minimum(2) }
end
describe 'associations' do
it { should have_many(:articles) }
it { should embed_one(:profile) }
it { should embed_many(:addresses) }
end
describe 'indexes' do
it { should have_index_for(email: 1) }
it { should have_index_for(name: 1, age: 1) }
end
describe 'scopes and methods' do
let!(:active_user) { create(:user, active: true) }
let!(:inactive_user) { create(:user, active: false) }
it 'finds only active users' do
expect(User.where(active: true)).to include(active_user)
expect(User.where(active: true)).not_to include(inactive_user)
end
end
end
# Factoryの定義
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.email }
age { rand(18..80) }
active { true }
trait :with_profile do
after(:create) do |user|
user.create_profile(
bio: Faker::Lorem.paragraph,
website: Faker::Internet.url
)
end
end
trait :with_articles do
after(:create) do |user|
create_list(:article, 3, user: user)
end
end
end
end