Mongoid

Mongoidは「Ruby用のエレガントな MongoDB ODM」として開発された、Ruby on RailsのActiveRecordライクなAPIを提供するMongoDB用Object-Document Mapper。Railsとのシームレスな統合、MongoDBのネイティブ機能を活かした柔軟なデータモデリング、パフォーマンスを重視した設計が特徴。スキーマレスなMongoDBの特性を生かし、リレーション管理、バリデーション、クエリ最適化等豊富な機能を提供し、Railsアプリケーション開発に最適化されています。

ODMRubyMongoDBActiveRecordNoSQLスキーマレス

GitHub概要

mongodb/mongoid

The Official Ruby Object Mapper for MongoDB

スター3,915
ウォッチ110
フォーク1,383
作成日:2009年9月16日
言語:Ruby
ライセンス:MIT License

トピックス

mongodbmongodb-driverormorm-frameworkrubyruby-on-rails

スター履歴

mongodb/mongoid Star History
データ取得日時: 2025/7/19 10:35

ライブラリ

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