MongoDB Atlas

データベースMongoDBNoSQLドキュメント指向クラウドマネージドサービス分散データベース

データベースプラットフォーム

MongoDB Atlas

概要

MongoDB AtlasはMongoDBのフルマネージドクラウドデータベースサービスです。グローバル分散、自動スケーリング、組み込みセキュリティ機能を統合し、NoSQLアプリケーション開発を大幅に簡素化します。開発者フレンドリーなドキュメント指向モデルと柔軟なスキーマにより、モダンアプリケーション開発で広く採用されています。AWS、Google Cloud、Microsoft Azureの複数クラウドプロバイダーで利用可能です。

詳細

ドキュメント指向データベース

MongoDBは柔軟なJSONライクなドキュメント形式でデータを格納し、複雑なデータ構造を自然に表現できます。スキーマレスの設計により、アプリケーションの進化に合わせてデータモデルを容易に変更できます。

グローバル分散とシャーディング

データを複数のリージョンとデータセンターに自動分散し、高可用性と災害復旧を実現します。水平スケーリング(シャーディング)により、大規模なデータセットとトラフィックに対応できます。

Atlas Search とデータレイク

全文検索、ファセット検索、オートコンプリート機能を内蔵し、Elasticsearchなしに高度な検索機能を実装できます。Atlas Data Lakeにより、アーカイブデータに対する分析クエリも効率的に実行できます。

組み込みセキュリティとコンプライアンス

暗号化、ネットワーク分離、アクセス制御、監査ログを標準提供し、GDPR、HIPAA、SOC 2などの規制要件に対応します。

メリット・デメリット

メリット

  • フルマネージドサービス: インフラ管理、バックアップ、監視を完全自動化
  • 柔軟なスキーマ: アプリケーション開発の初期段階から本格運用まで対応
  • 強力なクエリ機能: 豊富な集約パイプラインと地理空間クエリをサポート
  • 自動スケーリング: 需要に応じたコンピュートとストレージの動的調整
  • マルチクラウド対応: AWS、GCP、Azureで一貫した体験を提供
  • 豊富な統合: BI ツール、アナリティクス、機械学習プラットフォームとの連携
  • 開発者エクスペリエンス: 直感的なWeb UI、CLI、豊富なSDKを提供

デメリット

  • SQL非対応: 既存のSQLスキルや RDBMS ツールが直接活用できない
  • ACID制限: 複雑なトランザクション処理でRDBMSより制約がある
  • メモリ使用量: WiredTigerストレージエンジンで大きなメモリ使用量が必要
  • ベンダーロックイン: MongoDB特有の機能への依存が移行を困難にする
  • 学習コスト: NoSQLとドキュメント指向DBの概念理解が必要
  • 高トラフィック時のコスト: 大規模利用では従来の自己管理型より高価

参考ページ

実装の例

セットアップ

# MongoDB CLIをインストール
npm install -g mongodb-atlas-cli

# Atlasにログイン
atlas auth login

# 新しいクラスターを作成
atlas clusters create myCluster --provider AWS --region US_EAST_1

# Node.js ドライバーをインストール
npm install mongodb

# 接続文字列を取得
atlas clusters describe myCluster --output json

スキーマ設計

// コレクション設計例

// ユーザーコレクション
const userSchema = {
  _id: ObjectId,
  email: String,
  username: String,
  profile: {
    firstName: String,
    lastName: String,
    avatar: String,
    bio: String
  },
  settings: {
    theme: String,
    notifications: Boolean,
    privacy: String
  },
  createdAt: Date,
  updatedAt: Date
}

// 投稿コレクション(埋め込みコメント)
const postSchema = {
  _id: ObjectId,
  authorId: ObjectId,
  title: String,
  content: String,
  tags: [String],
  status: String, // "draft", "published", "archived"
  metadata: {
    views: Number,
    likes: Number,
    readTime: Number
  },
  comments: [{
    _id: ObjectId,
    authorId: ObjectId,
    content: String,
    createdAt: Date,
    replies: [{
      _id: ObjectId,
      authorId: ObjectId,
      content: String,
      createdAt: Date
    }]
  }],
  createdAt: Date,
  updatedAt: Date
}

// インデックス作成
db.users.createIndex({ email: 1 }, { unique: true })
db.users.createIndex({ username: 1 }, { unique: true })
db.posts.createIndex({ authorId: 1, status: 1 })
db.posts.createIndex({ tags: 1 })
db.posts.createIndex({ createdAt: -1 })

// 複合インデックス
db.posts.createIndex({ 
  "status": 1, 
  "createdAt": -1,
  "metadata.likes": -1 
})

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

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

データ操作

import { MongoClient, ObjectId } from 'mongodb'

const uri = process.env.MONGODB_URI
const client = new MongoClient(uri)

async function connectToDatabase() {
  await client.connect()
  const db = client.db('myapp')
  return db
}

// CRUD操作
class UserService {
  constructor(db) {
    this.collection = db.collection('users')
  }

  async createUser(userData) {
    const user = {
      ...userData,
      createdAt: new Date(),
      updatedAt: new Date()
    }
    
    const result = await this.collection.insertOne(user)
    return { ...user, _id: result.insertedId }
  }

  async getUserById(id) {
    return await this.collection.findOne({ _id: new ObjectId(id) })
  }

  async updateUser(id, updates) {
    const result = await this.collection.updateOne(
      { _id: new ObjectId(id) },
      { 
        $set: { 
          ...updates, 
          updatedAt: new Date() 
        }
      }
    )
    return result.modifiedCount > 0
  }

  async deleteUser(id) {
    const result = await this.collection.deleteOne({ 
      _id: new ObjectId(id) 
    })
    return result.deletedCount > 0
  }

  // 複雑なクエリ例
  async getUsersWithStats() {
    return await this.collection.aggregate([
      {
        $lookup: {
          from: 'posts',
          localField: '_id',
          foreignField: 'authorId',
          as: 'posts'
        }
      },
      {
        $addFields: {
          postCount: { $size: '$posts' },
          totalLikes: { 
            $sum: '$posts.metadata.likes' 
          }
        }
      },
      {
        $project: {
          username: 1,
          email: 1,
          postCount: 1,
          totalLikes: 1,
          createdAt: 1
        }
      },
      {
        $sort: { totalLikes: -1 }
      }
    ]).toArray()
  }
}

// 投稿サービス
class PostService {
  constructor(db) {
    this.collection = db.collection('posts')
  }

  async createPost(postData) {
    const post = {
      ...postData,
      metadata: {
        views: 0,
        likes: 0,
        readTime: this.calculateReadTime(postData.content)
      },
      comments: [],
      createdAt: new Date(),
      updatedAt: new Date()
    }
    
    const result = await this.collection.insertOne(post)
    return { ...post, _id: result.insertedId }
  }

  async addComment(postId, comment) {
    const newComment = {
      _id: new ObjectId(),
      ...comment,
      createdAt: new Date(),
      replies: []
    }

    await this.collection.updateOne(
      { _id: new ObjectId(postId) },
      { 
        $push: { comments: newComment },
        $inc: { 'metadata.views': 1 }
      }
    )

    return newComment
  }

  async searchPosts(query, options = {}) {
    const pipeline = []

    // テキスト検索
    if (query) {
      pipeline.push({
        $match: {
          $text: { $search: query }
        }
      })
    }

    // フィルター
    if (options.tags && options.tags.length > 0) {
      pipeline.push({
        $match: {
          tags: { $in: options.tags }
        }
      })
    }

    // ソート
    pipeline.push({
      $sort: options.sortBy || { createdAt: -1 }
    })

    // ページネーション
    if (options.skip) {
      pipeline.push({ $skip: options.skip })
    }
    if (options.limit) {
      pipeline.push({ $limit: options.limit })
    }

    return await this.collection.aggregate(pipeline).toArray()
  }

  calculateReadTime(content) {
    const wordsPerMinute = 200
    const wordCount = content.split(' ').length
    return Math.ceil(wordCount / wordsPerMinute)
  }
}

スケーリング

// シャーディング設定
class DatabaseManager {
  constructor() {
    this.client = new MongoClient(process.env.MONGODB_URI, {
      maxPoolSize: 100,
      minPoolSize: 5,
      maxIdleTimeMS: 30000,
      serverSelectionTimeoutMS: 5000,
    })
  }

  async setupSharding() {
    const admin = this.client.db('admin')
    
    // シャーディングを有効化
    await admin.command({ enableSharding: 'myapp' })
    
    // シャードキーを設定
    await admin.command({
      shardCollection: 'myapp.posts',
      key: { authorId: 1, createdAt: 1 }
    })
  }

  // 読み取り設定の最適化
  async getReadOnlyConnection() {
    return new MongoClient(process.env.MONGODB_URI, {
      readPreference: 'secondary',
      readConcern: { level: 'available' }
    })
  }

  // バッチ処理の最適化
  async batchInsert(collection, documents, batchSize = 1000) {
    const batches = []
    for (let i = 0; i < documents.length; i += batchSize) {
      batches.push(documents.slice(i, i + batchSize))
    }

    const results = []
    for (const batch of batches) {
      const result = await collection.insertMany(batch, {
        ordered: false,
        writeConcern: { w: 'majority', j: true }
      })
      results.push(result)
    }

    return results
  }
}

バックアップ・復旧

// MongoDB Atlasの自動バックアップは管理コンソールで設定
// プログラマティックなバックアップ操作

class BackupManager {
  constructor(db) {
    this.db = db
  }

  async exportCollection(collectionName, query = {}) {
    const collection = this.db.collection(collectionName)
    const cursor = collection.find(query)
    
    const documents = []
    await cursor.forEach(doc => {
      documents.push(doc)
    })
    
    return {
      collection: collectionName,
      count: documents.length,
      data: documents,
      exportedAt: new Date()
    }
  }

  async importCollection(collectionName, data) {
    const collection = this.db.collection(collectionName)
    
    // 既存データのクリア(オプション)
    // await collection.deleteMany({})
    
    if (data.length > 0) {
      const result = await collection.insertMany(data, {
        ordered: false
      })
      return result.insertedCount
    }
    
    return 0
  }

  async createPointInTimeSnapshot() {
    // Atlas APIを使用したスナップショット作成
    const response = await fetch(
      `https://cloud.mongodb.com/api/atlas/v1.0/groups/${process.env.ATLAS_PROJECT_ID}/clusters/${process.env.CLUSTER_NAME}/backup/snapshots`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.ATLAS_API_TOKEN}`
        },
        body: JSON.stringify({
          description: `Manual snapshot ${new Date().toISOString()}`,
          retentionInDays: 7
        })
      }
    )
    
    return await response.json()
  }
}

統合

// Express.js REST API
import express from 'express'
import { MongoClient } from 'mongodb'

const app = express()
app.use(express.json())

let db

MongoClient.connect(process.env.MONGODB_URI)
  .then(client => {
    db = client.db('myapp')
    console.log('Connected to MongoDB Atlas')
  })

// REST エンドポイント
app.get('/api/posts', async (req, res) => {
  try {
    const { page = 1, limit = 10, tag, search } = req.query
    const skip = (page - 1) * limit

    const query = { status: 'published' }
    if (tag) query.tags = tag
    if (search) query.$text = { $search: search }

    const posts = await db.collection('posts')
      .find(query)
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(parseInt(limit))
      .toArray()

    const total = await db.collection('posts').countDocuments(query)

    res.json({
      posts,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        pages: Math.ceil(total / limit)
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// GraphQL との統合
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'

const typeDefs = `
  type User {
    id: ID!
    username: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    createdAt: String!
  }

  type Query {
    users: [User!]!
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createPost(title: String!, content: String!, authorId: ID!): Post!
  }
`

const resolvers = {
  Query: {
    users: async () => {
      return await db.collection('users').find({}).toArray()
    },
    posts: async () => {
      return await db.collection('posts')
        .aggregate([
          {
            $lookup: {
              from: 'users',
              localField: 'authorId',
              foreignField: '_id',
              as: 'author'
            }
          },
          { $unwind: '$author' }
        ])
        .toArray()
    }
  },
  Mutation: {
    createPost: async (_, { title, content, authorId }) => {
      const post = {
        title,
        content,
        authorId: new ObjectId(authorId),
        createdAt: new Date()
      }
      
      const result = await db.collection('posts').insertOne(post)
      return { ...post, id: result.insertedId }
    }
  }
}

const server = new ApolloServer({ typeDefs, resolvers })