11
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

「テーブル設計が複雑すぎる」「スキーマの変更が大変」「大量のデータを高速に処理したい」

これらの課題を解決するのがドキュメント指向データベースのMongoDBです。柔軟なスキーマ、高いスケーラビリティ、そしてJSONライクなデータ構造により、現代のアプリケーション開発で広く採用されています。

この記事では、MongoDBの基礎から実践的なクエリ、パフォーマンス最適化まで詳しく解説します。

MongoDBは便利ですが「RDBが辛いから」で導入すると事故りがちです。
実務で重要なのは

  • どのデータをMongoDBに置くべきか
  • スキーマレスをどう運用するか
  • 埋め込みと参照をどう決めるか

の判断です。
この記事では基本説明に入る前に、まず意思決定の軸を置きます。

まず決める採用判断の軸

MongoDBが向くケース

  • 取得単位がドキュメントで自然にまとまる(プロフィール、設定、コンテンツ)
  • スキーマが進化しやすい(フィールド追加が頻繁)
  • 読みが中心で、ドキュメント単位で完結しやすい

向かないケース

  • 厳密な整合性が最優先(残高、在庫、二重更新が致命的)
  • 複雑な結合や集計が主戦場で、ドキュメント境界が定まらない
  • 更新が多く、ドキュメントが肥大化しやすい

埋め込み 参照 を決める判断軸

迷ったら次で判断するとブレません。

  • 一緒に取得する頻度が高いか
  • 一緒に更新する必要があるか
  • サイズは増え続けるか(配列が無限に伸びないか)

基本戦略

  • 基本は埋め込みで読みを速くする
  • 増え続ける配列や独立更新が必要なら参照に逃がす

スキーマレス運用の落とし穴

スキーマレスは「スキーマがない」ではなく「スキーマがコードとデータに散る」という意味です。
運用で壊れやすいポイントは

  • 書き込み経路が複数あって形が揺れる
  • 型が揃わず、集計や検索が不安定になる
  • optional フィールドの扱いが曖昧で、アプリ側が複雑化する

これを避けるには

  • 書き込み時のバリデーション
  • バージョニング(schemaVersion)
  • 移行手順(バックフィル)

をセットで持つのがコツです。

最小チェックリスト(設計レビュー用)

  • ドキュメント境界が説明できる
  • 増え続ける配列を埋め込んでいない
  • schemaVersion と移行方針がある
  • インデックス設計がある(検索条件に合わせる)
  • バックアップとリストア手順がある

MongoDBとは

なぜMongoDBが必要なのか

RDBMSは長年にわたりデータベースの標準でした。しかし、現代のアプリケーション開発では以下のような課題が生じることがあります。

  • スキーマ変更のコスト: テーブル構造を変更するたびにALTER TABLEが必要。大規模なテーブルでは数時間かかることも
  • 複雑なJOIN: 正規化されたデータを取得するのに何テーブルも結合が必要
  • スケールの難しさ: 単一サーバーの性能限界に達したとき、水平スケールが困難

MongoDBは**「ドキュメント指向」**という異なるアプローチでこれらを解決します。

ドキュメント指向とは?

データをテーブルの行ではなく、JSONのようなドキュメントとして保存します。1つのドキュメントに関連データをすべて含められるため、JOINが不要になることが多いです。

// RDBMSの場合: 3つのテーブルをJOINして取得
// users + addresses + orders → 複雑なクエリ

// MongoDBの場合: 1つのドキュメントに含める
{
    "name": "田中太郎",
    "address": { "city": "東京", "zipcode": "100-0001" },
    "orders": [
        { "product": "商品A", "price": 1000 },
        { "product": "商品B", "price": 2000 }
    ]
}

いつMongoDBを選ぶべきか?

ユースケース MongoDB向き RDBMS向き
スキーマが頻繁に変わる
階層的なデータ構造
大量の読み書き ◎(水平スケール)
複雑なトランザクション
厳密なデータ整合性

MongoDBは「銀の弾丸」ではありません。トランザクションが複雑な銀行システムや、厳密な整合性が必要な在庫管理にはRDBMSが適しています。一方、コンテンツ管理、ユーザープロフィール、IoTデータ、ログ収集などにはMongoDBが威力を発揮します。

RDBMSとの比較

特徴 RDBMS(MySQL等) MongoDB
データモデル テーブル(行と列) ドキュメント(JSON/BSON)
スキーマ 固定(事前定義必要) 柔軟(スキーマレス)
結合 JOINで実現 埋め込みor参照
スケーリング 垂直スケール中心 水平スケール(シャーディング)
トランザクション 完全サポート サポート(4.0以降)
ユースケース 複雑な関係データ 柔軟なデータ構造

用語の対応

RDBMSの知識がある方は、以下の用語対応を覚えておくと理解しやすいです。

RDBMS MongoDB 説明
Database Database データベース(同じ概念)
Table Collection ドキュメントのグループ
Row Document 1つのデータレコード(JSONオブジェクト)
Column Field ドキュメント内の属性
Primary Key _id 一意識別子(自動生成)
Index Index 検索高速化(同じ概念)
JOIN $lookup / 埋め込み 関連データの取得方法

ドキュメント構造

MongoDBのドキュメントは**BSON(Binary JSON)**形式で保存されます。JSONと似ていますが、日付型やObjectIdなど追加の型をサポートしています。

// MongoDBのドキュメント例
{
    // _id: 一意識別子。指定しなければ自動生成される
    // ObjectIdは12バイトで、タイムスタンプ+マシンID+プロセスID+カウンター
    "_id": ObjectId("507f1f77bcf86cd799439011"),
    
    // 基本的なフィールド
    "name": "田中太郎",
    "email": "tanaka@example.com",
    "age": 30,
    
    // 埋め込みドキュメント(ネスト構造)
    // 関連データを1つのドキュメントにまとめられる
    "address": {
        "city": "東京",
        "zipcode": "100-0001"
    },
    
    // 配列(複数の値を保持)
    "tags": ["developer", "javascript", "mongodb"],
    
    // 日付型(ISODate)
    "createdAt": ISODate("2024-01-01T00:00:00Z")
}

埋め込み vs 参照

関連データの持ち方には2つのアプローチがあります:

// 埋め込み(Embedding): 1つのドキュメントに含める
// メリット: 1回のクエリで取得可能
// デメリット: ドキュメントサイズが大きくなる(上限16MB)
{
    "_id": ObjectId("..."),
    "name": "田中太郎",
    "orders": [
        { "product": "商品A", "price": 1000 },
        { "product": "商品B", "price": 2000 }
    ]
}

// 参照(Reference): 別コレクションへの参照
// メリット: 柔軟性が高い、重複を避けられる
// デメリット: $lookupが必要(性能コスト)
{
    "_id": ObjectId("user1"),
    "name": "田中太郎",
    "orderIds": [ObjectId("order1"), ObjectId("order2")]
}

選択の指針: 「一緒に取得することが多いデータ」は埋め込み、「独立して操作することが多いデータ」は参照がおすすめです。


# 環境構築

## インストール

```bash
# macOS (Homebrew)
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community

# Ubuntu
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo systemctl start mongod

# Docker
docker run -d --name mongodb -p 27017:27017 \
    -e MONGO_INITDB_ROOT_USERNAME=admin \
    -e MONGO_INITDB_ROOT_PASSWORD=password \
    mongo:latest

# Docker Compose
# docker-compose.yml
version: '3.8'
services:
  mongodb:
    image: mongo:latest
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password
    volumes:
      - mongodb_data:/data/db

volumes:
  mongodb_data:

接続

# MongoDB Shellで接続
mongosh

# 認証付き接続
mongosh "mongodb://admin:password@localhost:27017"

# 接続文字列の例
mongosh "mongodb+srv://cluster.mongodb.net/mydb" --username user

基本操作(CRUD)

データベースとコレクション

// データベース一覧
show dbs

// データベースを選択/作成
use myapp

// コレクション一覧
show collections

// コレクションの作成(明示的)
db.createCollection("users")

// コレクションの削除
db.users.drop()

// データベースの削除
db.dropDatabase()

ドキュメントの挿入(Create)

// 1件挿入
db.users.insertOne({
    name: "田中太郎",
    email: "tanaka@example.com",
    age: 30,
    createdAt: new Date()
})

// 複数件挿入
db.users.insertMany([
    {
        name: "佐藤花子",
        email: "sato@example.com",
        age: 25
    },
    {
        name: "鈴木一郎",
        email: "suzuki@example.com",
        age: 35
    }
])

// 挿入結果
// {
//     acknowledged: true,
//     insertedId: ObjectId("...")
// }

ドキュメントの検索(Read)

// 全件取得
db.users.find()

// 整形して表示
db.users.find().pretty()

// 1件取得
db.users.findOne({ name: "田中太郎" })

// 条件付き検索
db.users.find({ age: { $gt: 25 } })

// 複数条件(AND)
db.users.find({
    age: { $gte: 25, $lte: 35 },
    "address.city": "東京"
})

// OR条件
db.users.find({
    $or: [
        { age: { $lt: 25 } },
        { age: { $gt: 50 } }
    ]
})

// フィールド指定(プロジェクション)
db.users.find(
    { age: { $gt: 25 } },
    { name: 1, email: 1, _id: 0 }
)

// ソート
db.users.find().sort({ age: -1 })  // 降順
db.users.find().sort({ name: 1 })  // 昇順

// リミットとスキップ(ページネーション)
db.users.find().skip(10).limit(10)

// カウント
db.users.countDocuments({ age: { $gt: 25 } })

比較演算子

演算子 意味
$eq 等しい { age: { $eq: 25 } }
$ne 等しくない { status: { $ne: "inactive" } }
$gt より大きい { age: { $gt: 25 } }
$gte 以上 { age: { $gte: 25 } }
$lt より小さい { age: { $lt: 30 } }
$lte 以下 { age: { $lte: 30 } }
$in いずれかに一致 { status: { $in: ["active", "pending"] } }
$nin いずれにも一致しない { status: { $nin: ["deleted"] } }

論理演算子

// AND(暗黙的)
db.users.find({ age: { $gte: 20 }, status: "active" })

// AND(明示的)
db.users.find({
    $and: [
        { age: { $gte: 20 } },
        { age: { $lte: 30 } }
    ]
})

// OR
db.users.find({
    $or: [
        { status: "active" },
        { premium: true }
    ]
})

// NOT
db.users.find({
    age: { $not: { $gt: 30 } }
})

// NOR(すべての条件に一致しない)
db.users.find({
    $nor: [
        { status: "deleted" },
        { banned: true }
    ]
})

配列とネストドキュメントの検索

// 配列に要素が含まれる
db.users.find({ tags: "developer" })

// 配列のすべての要素が一致
db.users.find({ tags: { $all: ["developer", "javascript"] } })

// 配列のサイズ
db.users.find({ tags: { $size: 3 } })

// 配列の要素に対する条件
db.users.find({ "scores": { $elemMatch: { $gte: 80, $lt: 90 } } })

// ネストドキュメント
db.users.find({ "address.city": "東京" })
db.users.find({ "address.zipcode": { $regex: /^100/ } })

ドキュメントの更新(Update)

// 1件更新
db.users.updateOne(
    { email: "tanaka@example.com" },
    { $set: { age: 31, updatedAt: new Date() } }
)

// 複数件更新
db.users.updateMany(
    { status: "pending" },
    { $set: { status: "active" } }
)

// フィールドの追加/更新
db.users.updateOne(
    { _id: ObjectId("...") },
    { $set: { "address.city": "大阪" } }
)

// フィールドの削除
db.users.updateOne(
    { _id: ObjectId("...") },
    { $unset: { temporaryField: "" } }
)

// 数値のインクリメント
db.users.updateOne(
    { _id: ObjectId("...") },
    { $inc: { loginCount: 1, points: 100 } }
)

// 配列に要素を追加
db.users.updateOne(
    { _id: ObjectId("...") },
    { $push: { tags: "newTag" } }
)

// 配列に複数要素を追加(重複なし)
db.users.updateOne(
    { _id: ObjectId("...") },
    { $addToSet: { tags: { $each: ["tag1", "tag2"] } } }
)

// 配列から要素を削除
db.users.updateOne(
    { _id: ObjectId("...") },
    { $pull: { tags: "oldTag" } }
)

// 存在しなければ挿入(Upsert)
db.users.updateOne(
    { email: "new@example.com" },
    { $set: { name: "新規ユーザー", createdAt: new Date() } },
    { upsert: true }
)

// ドキュメント全体を置換
db.users.replaceOne(
    { _id: ObjectId("...") },
    { name: "新しい名前", email: "new@example.com" }
)

ドキュメントの削除(Delete)

// 1件削除
db.users.deleteOne({ email: "tanaka@example.com" })

// 複数件削除
db.users.deleteMany({ status: "deleted" })

// 全件削除
db.users.deleteMany({})

// 条件付き削除
db.users.deleteMany({
    lastLoginAt: { $lt: new Date("2023-01-01") },
    status: "inactive"
})

Aggregation Pipeline

集計処理のための強力なフレームワークです。

基本構造

db.collection.aggregate([
    { $stage1: { ... } },
    { $stage2: { ... } },
    { $stage3: { ... } }
])

主要なステージ

// $match: フィルタリング
db.orders.aggregate([
    { $match: { status: "completed", createdAt: { $gte: new Date("2024-01-01") } } }
])

// $group: グループ化と集計
db.orders.aggregate([
    { $match: { status: "completed" } },
    {
        $group: {
            _id: "$customerId",
            totalAmount: { $sum: "$amount" },
            orderCount: { $sum: 1 },
            avgAmount: { $avg: "$amount" },
            maxAmount: { $max: "$amount" },
            minAmount: { $min: "$amount" }
        }
    }
])

// $sort: ソート
db.orders.aggregate([
    { $match: { status: "completed" } },
    { $group: { _id: "$customerId", total: { $sum: "$amount" } } },
    { $sort: { total: -1 } }
])

// $limit と $skip: ページネーション
db.orders.aggregate([
    { $sort: { createdAt: -1 } },
    { $skip: 20 },
    { $limit: 10 }
])

// $project: フィールドの選択と変換
db.users.aggregate([
    {
        $project: {
            fullName: { $concat: ["$firstName", " ", "$lastName"] },
            email: 1,
            age: 1,
            _id: 0
        }
    }
])

// $unwind: 配列の展開
db.orders.aggregate([
    { $unwind: "$items" },
    {
        $group: {
            _id: "$items.productId",
            totalQuantity: { $sum: "$items.quantity" },
            totalRevenue: { $sum: { $multiply: ["$items.price", "$items.quantity"] } }
        }
    }
])

// $lookup: 結合(LEFT OUTER JOIN相当)
db.orders.aggregate([
    {
        $lookup: {
            from: "users",
            localField: "customerId",
            foreignField: "_id",
            as: "customer"
        }
    },
    { $unwind: "$customer" },
    {
        $project: {
            orderNumber: 1,
            amount: 1,
            "customer.name": 1,
            "customer.email": 1
        }
    }
])

// $addFields: フィールドの追加
db.orders.aggregate([
    {
        $addFields: {
            totalWithTax: { $multiply: ["$amount", 1.1] },
            isLargeOrder: { $gte: ["$amount", 10000] }
        }
    }
])

// $bucket: 範囲でグループ化
db.users.aggregate([
    {
        $bucket: {
            groupBy: "$age",
            boundaries: [0, 20, 30, 40, 50, 100],
            default: "Other",
            output: {
                count: { $sum: 1 },
                avgAge: { $avg: "$age" }
            }
        }
    }
])

// $facet: 複数の集計を並列実行
db.products.aggregate([
    {
        $facet: {
            "byCategory": [
                { $group: { _id: "$category", count: { $sum: 1 } } }
            ],
            "priceStats": [
                {
                    $group: {
                        _id: null,
                        avgPrice: { $avg: "$price" },
                        maxPrice: { $max: "$price" },
                        minPrice: { $min: "$price" }
                    }
                }
            ],
            "topProducts": [
                { $sort: { sales: -1 } },
                { $limit: 5 }
            ]
        }
    }
])

実践的な集計例

// 月別売上レポート
db.orders.aggregate([
    { $match: { status: "completed" } },
    {
        $group: {
            _id: {
                year: { $year: "$createdAt" },
                month: { $month: "$createdAt" }
            },
            totalRevenue: { $sum: "$amount" },
            orderCount: { $sum: 1 },
            avgOrderValue: { $avg: "$amount" }
        }
    },
    { $sort: { "_id.year": -1, "_id.month": -1 } },
    {
        $project: {
            _id: 0,
            yearMonth: {
                $concat: [
                    { $toString: "$_id.year" },
                    "-",
                    { $cond: [{ $lt: ["$_id.month", 10] }, { $concat: ["0", { $toString: "$_id.month" }] }, { $toString: "$_id.month" }] }
                ]
            },
            totalRevenue: { $round: ["$totalRevenue", 0] },
            orderCount: 1,
            avgOrderValue: { $round: ["$avgOrderValue", 0] }
        }
    }
])

// ユーザーの購入履歴分析
db.orders.aggregate([
    {
        $lookup: {
            from: "users",
            localField: "customerId",
            foreignField: "_id",
            as: "customer"
        }
    },
    { $unwind: "$customer" },
    {
        $group: {
            _id: "$customerId",
            customerName: { $first: "$customer.name" },
            customerEmail: { $first: "$customer.email" },
            totalSpent: { $sum: "$amount" },
            orderCount: { $sum: 1 },
            firstOrder: { $min: "$createdAt" },
            lastOrder: { $max: "$createdAt" },
            products: { $push: "$items.productId" }
        }
    },
    {
        $addFields: {
            avgOrderValue: { $divide: ["$totalSpent", "$orderCount"] },
            daysSinceLastOrder: {
                $divide: [
                    { $subtract: [new Date(), "$lastOrder"] },
                    1000 * 60 * 60 * 24
                ]
            }
        }
    },
    { $sort: { totalSpent: -1 } },
    { $limit: 100 }
])

インデックス

インデックスの作成

// 単一フィールドインデックス
db.users.createIndex({ email: 1 })  // 昇順
db.users.createIndex({ createdAt: -1 })  // 降順

// ユニークインデックス
db.users.createIndex({ email: 1 }, { unique: true })

// 複合インデックス
db.orders.createIndex({ customerId: 1, createdAt: -1 })

// 部分インデックス
db.orders.createIndex(
    { createdAt: 1 },
    { partialFilterExpression: { status: "active" } }
)

// TTLインデックス(自動削除)
db.sessions.createIndex(
    { createdAt: 1 },
    { expireAfterSeconds: 3600 }  // 1時間後に削除
)

// テキストインデックス(全文検索)
db.articles.createIndex({ title: "text", content: "text" })

// 地理空間インデックス
db.locations.createIndex({ coordinates: "2dsphere" })

// ハッシュインデックス(シャーディング用)
db.users.createIndex({ email: "hashed" })

インデックスの管理

// インデックス一覧
db.users.getIndexes()

// インデックスの削除
db.users.dropIndex("email_1")

// 全インデックスの削除(_idを除く)
db.users.dropIndexes()

// インデックスの使用状況
db.users.aggregate([{ $indexStats: {} }])

実行計画の確認

// explain()で実行計画を確認
db.users.find({ email: "test@example.com" }).explain()

// 詳細な実行計画
db.users.find({ email: "test@example.com" }).explain("executionStats")

// 重要な指標
// - COLLSCAN: コレクション全スキャン(インデックスなし)
// - IXSCAN: インデックススキャン
// - totalDocsExamined: 調べたドキュメント数
// - executionTimeMillis: 実行時間

Node.jsでの使用

インストールと接続

// npm install mongodb

const { MongoClient, ObjectId } = require('mongodb');

const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);

async function main() {
    try {
        await client.connect();
        console.log('Connected to MongoDB');
        
        const db = client.db('myapp');
        const users = db.collection('users');
        
        // 操作を実行
        
    } finally {
        await client.close();
    }
}

main().catch(console.error);

CRUD操作

const { MongoClient, ObjectId } = require('mongodb');

class UserRepository {
    constructor(db) {
        this.collection = db.collection('users');
    }
    
    // 作成
    async create(userData) {
        const result = await this.collection.insertOne({
            ...userData,
            createdAt: new Date(),
            updatedAt: new Date()
        });
        return result.insertedId;
    }
    
    // ID検索
    async findById(id) {
        return await this.collection.findOne({
            _id: new ObjectId(id)
        });
    }
    
    // 条件検索
    async findByEmail(email) {
        return await this.collection.findOne({ email });
    }
    
    // 一覧取得
    async findAll(filter = {}, options = {}) {
        const { page = 1, limit = 10, sort = { createdAt: -1 } } = options;
        const skip = (page - 1) * limit;
        
        const [users, total] = await Promise.all([
            this.collection
                .find(filter)
                .sort(sort)
                .skip(skip)
                .limit(limit)
                .toArray(),
            this.collection.countDocuments(filter)
        ]);
        
        return {
            users,
            pagination: {
                page,
                limit,
                total,
                totalPages: Math.ceil(total / limit)
            }
        };
    }
    
    // 更新
    async update(id, updateData) {
        const result = await this.collection.updateOne(
            { _id: new ObjectId(id) },
            {
                $set: {
                    ...updateData,
                    updatedAt: new Date()
                }
            }
        );
        return result.modifiedCount > 0;
    }
    
    // 削除
    async delete(id) {
        const result = await this.collection.deleteOne({
            _id: new ObjectId(id)
        });
        return result.deletedCount > 0;
    }
    
    // 検索
    async search(query, options = {}) {
        const { page = 1, limit = 10 } = options;
        
        const filter = {
            $or: [
                { name: { $regex: query, $options: 'i' } },
                { email: { $regex: query, $options: 'i' } }
            ]
        };
        
        return await this.findAll(filter, { page, limit });
    }
}

// 使用例
async function main() {
    const client = new MongoClient('mongodb://localhost:27017');
    await client.connect();
    
    const db = client.db('myapp');
    const userRepo = new UserRepository(db);
    
    // ユーザー作成
    const userId = await userRepo.create({
        name: '田中太郎',
        email: 'tanaka@example.com',
        age: 30
    });
    console.log('Created user:', userId);
    
    // ユーザー取得
    const user = await userRepo.findById(userId);
    console.log('Found user:', user);
    
    // 一覧取得
    const { users, pagination } = await userRepo.findAll({}, { page: 1, limit: 10 });
    console.log('Users:', users);
    console.log('Pagination:', pagination);
    
    await client.close();
}

Mongoose(ODM)

// npm install mongoose

const mongoose = require('mongoose');

// スキーマ定義
const userSchema = new mongoose.Schema({
    name: {
        type: String,
        required: [true, '名前は必須です'],
        trim: true,
        maxlength: [100, '名前は100文字以内で入力してください']
    },
    email: {
        type: String,
        required: [true, 'メールアドレスは必須です'],
        unique: true,
        lowercase: true,
        match: [/^\S+@\S+\.\S+$/, '有効なメールアドレスを入力してください']
    },
    password: {
        type: String,
        required: true,
        minlength: 8,
        select: false  // デフォルトで取得しない
    },
    age: {
        type: Number,
        min: [0, '年齢は0以上で入力してください'],
        max: [150, '年齢は150以下で入力してください']
    },
    role: {
        type: String,
        enum: ['user', 'admin', 'moderator'],
        default: 'user'
    },
    profile: {
        bio: String,
        avatar: String,
        website: String
    },
    tags: [String],
    isActive: {
        type: Boolean,
        default: true
    }
}, {
    timestamps: true,  // createdAt, updatedAtを自動追加
    toJSON: { virtuals: true },
    toObject: { virtuals: true }
});

// インデックス
userSchema.index({ email: 1 });
userSchema.index({ createdAt: -1 });
userSchema.index({ name: 'text', 'profile.bio': 'text' });

// バーチャルフィールド
userSchema.virtual('fullName').get(function() {
    return `${this.firstName} ${this.lastName}`;
});

// インスタンスメソッド
userSchema.methods.isAdmin = function() {
    return this.role === 'admin';
};

// 静的メソッド
userSchema.statics.findByEmail = function(email) {
    return this.findOne({ email });
};

// ミドルウェア(フック)
userSchema.pre('save', async function(next) {
    if (this.isModified('password')) {
        const bcrypt = require('bcrypt');
        this.password = await bcrypt.hash(this.password, 10);
    }
    next();
});

// モデル作成
const User = mongoose.model('User', userSchema);

// 使用例
async function main() {
    await mongoose.connect('mongodb://localhost:27017/myapp');
    
    // 作成
    const user = await User.create({
        name: '田中太郎',
        email: 'tanaka@example.com',
        password: 'password123',
        age: 30,
        tags: ['developer', 'javascript']
    });
    
    // 検索
    const foundUser = await User.findById(user._id);
    const userByEmail = await User.findByEmail('tanaka@example.com');
    
    // 更新
    await User.findByIdAndUpdate(user._id, { age: 31 }, { new: true });
    
    // クエリビルダー
    const activeUsers = await User.find()
        .where('isActive').equals(true)
        .where('age').gte(20).lte(40)
        .select('name email age')
        .sort('-createdAt')
        .limit(10);
    
    // 全文検索
    const searchResults = await User.find({
        $text: { $search: 'developer' }
    });
    
    // 集計
    const stats = await User.aggregate([
        { $match: { isActive: true } },
        {
            $group: {
                _id: '$role',
                count: { $sum: 1 },
                avgAge: { $avg: '$age' }
            }
        }
    ]);
    
    await mongoose.disconnect();
}

データモデリング

埋め込み vs 参照

// 埋め込み(Embedding)
// 1対少数、頻繁に一緒に読み取る場合に適している
{
    "_id": ObjectId("..."),
    "title": "ブログ記事",
    "content": "...",
    "author": {
        "name": "田中太郎",
        "email": "tanaka@example.com"
    },
    "comments": [
        { "text": "いい記事ですね", "author": "佐藤" },
        { "text": "参考になりました", "author": "鈴木" }
    ]
}

// 参照(Reference)
// 1対多、独立して更新する場合に適している
// articles コレクション
{
    "_id": ObjectId("article1"),
    "title": "ブログ記事",
    "authorId": ObjectId("user1")
}

// comments コレクション
{
    "_id": ObjectId("comment1"),
    "articleId": ObjectId("article1"),
    "text": "いい記事ですね",
    "authorId": ObjectId("user2")
}

選択の指針

基準 埋め込み 参照
関係性 1対少数 1対多、多対多
読み取りパターン 一緒に読み取る 独立して読み取る
更新頻度 低い 高い
ドキュメントサイズ 16MB未満 大きくなる可能性
データの重複 許容する 避ける

パフォーマンス最適化

インデックス戦略

// 複合インデックスの順序
// クエリパターン: { status: "active", createdAt: { $gte: ... } }
// 正しい順序: 等価条件 → 範囲条件
db.orders.createIndex({ status: 1, createdAt: -1 })

// カバリングインデックス
// インデックスだけで結果を返せるようにする
db.users.createIndex({ email: 1, name: 1 })
db.users.find({ email: "test@example.com" }, { name: 1, _id: 0 })

// スパースインデックス
// nullや存在しないフィールドを含めない
db.users.createIndex({ nickname: 1 }, { sparse: true })

クエリ最適化

// プロジェクションで必要なフィールドのみ取得
db.users.find({}, { name: 1, email: 1 })

// 大量データの処理にはカーソルを使用
const cursor = db.users.find({}).batchSize(100);
while (await cursor.hasNext()) {
    const doc = await cursor.next();
    // 処理
}

// $existsよりスパースインデックス
// 遅い
db.users.find({ optionalField: { $exists: true } })

// 速い(スパースインデックスがある場合)
db.users.find({ optionalField: { $ne: null } })

書き込み最適化

// バルク操作
const bulk = db.users.initializeUnorderedBulkOp();
for (const user of users) {
    bulk.insert(user);
}
await bulk.execute();

// bulkWrite
await db.users.bulkWrite([
    { insertOne: { document: { name: "User1" } } },
    { updateOne: { filter: { _id: id1 }, update: { $set: { status: "active" } } } },
    { deleteOne: { filter: { _id: id2 } } }
]);

まとめ

この記事では、MongoDBの基礎から実践的なクエリまで解説しました。

基本操作

操作 コマンド
挿入 insertOne, insertMany
検索 find, findOne
更新 updateOne, updateMany
削除 deleteOne, deleteMany

集計パイプライン

ステージ 用途
$match フィルタリング
$group グループ化・集計
$sort ソート
$project フィールド選択・変換
$lookup 結合
$unwind 配列展開

ベストプラクティス

  1. 適切なインデックスを作成する
  2. 埋め込みと参照を適切に使い分ける
  3. プロジェクションで必要なフィールドのみ取得
  4. explain()で実行計画を確認
  5. バルク操作で書き込みを効率化

MongoDBは柔軟なデータモデリングと高いスケーラビリティを持つ強力なデータベースです。ぜひプロジェクトに活用してみてください!

11
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?