404 Not Found

404 Not Found


nginx

「第21課:MongoDBの基礎」

1. なぜMongoDBを選ぶのか

AliceのeコマースAPIは、厄介な問題に直面しています。それは、商品属性がカテゴリーごとに大きく異なるという点です。たとえば、ノートパソコンにはCPUやメモリのフィールドがあり、衣類にはサイズや色のフィールドがあり、食品には賞味期限のフィールドがあります。リレーショナルデータベースを使用する場合、新しいカテゴリーが追加されるたびにテーブル構造を変更する必要があります。一方、MongoDBのドキュメントモデルは、構造の異なるレコードを自然にサポートしています。各商品ドキュメントは、スキーマの移行を一切必要とせずに、まったく異なるフィールドを持つことができます。

SQLの基礎 MongoDBの基礎 説明
データベース データベース データベース(用語の統一)
テーブル コレクション テーブル → コレクション
ドキュメント 行 → ドキュメント
フィールド 列 → フィールド
主キー _id (ObjectId) 自動生成される主キー
JOIN $lookup (集計) さまざまな種類の結合クエリ
スキーマ スキーマの強制なし オプションの検証ルール
インデックス インデックス 類似のインデックス作成メカニズム

2. 設置と接続

(1) MongoDB ドライバーをインストールする

MongoDB サーバーに接続するには、公式の mongodb npm パッケージを使用してください。

▶ 例:ドライバのインストール

BASH
npm install mongodb

(2) クライアントとの接続を確立する

MongoClient は、MongoDB に接続するためのエントリポイントです。接続文字列を通じて、アドレスやオプションを指定します。

▶ 例:基本的な接続

JAVASCRIPT
const { MongoClient } = require('mongodb');

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

async function connect() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');
    const db = client.db('myapp');
    return db;
  } catch (err) {
    console.error('Connection failed:', err.message);
    process.exit(1);
  }
}

(3) コネクションプールと設定オプション

MongoClient 接続プールが組み込まれています。プールのサイズと動作はオプションで制御されます。

▶ 例:コネクションプールの設定を用いた接続

JAVASCRIPT
const client = new MongoClient(uri, {
  maxPoolSize: 10,
  minPoolSize: 2,
  maxIdleTimeMS: 30000,
  serverSelectionTimeoutMS: 5000,
  connectTimeoutMS: 10000,
});
設定オプション 既定値 説明
maxPoolSize 100 接続プールにおける最大接続数
minPoolSize 0 接続プール内の最小接続数
maxIdleTimeMS 0 アイドル状態の接続タイムアウト (0 = タイムアウトなし)
serverSelectionTimeoutMS 30000 サーバー選択のタイムアウト
connectTimeoutMS 30000 接続確立のタイムアウト
socketTimeoutMS 0 ソケットタイムアウト
retryWrites true 書き込み操作を自動的に再試行する

(4) 接続を適切に閉じる

アプリケーションが終了する際には、接続を閉じ、リソースを解放する必要があります。

▶ 例:正常なシャットダウン

JAVASCRIPT
process.on('SIGINT', async () => {
  await client.close();
  console.log('MongoDB connection closed');
  process.exit(0);
});

3. CRUD操作

(1) 文書を挿入する

insertOne を使用すると単一の文書を挿入でき、insertMany を使用すると複数の文書を挿入できます。

▶ 例:製品ドキュメントの挿入

JAVASCRIPT
const products = db.collection('products');

const result = await products.insertOne({
  name: 'Mechanical Keyboard',
  price: 89.99,
  category: 'electronics',
  specs: { switches: 'Cherry MX Blue', layout: 'ANSI' },
  createdAt: new Date(),
});

console.log('Inserted ID:', result.insertedId);

(2) ドキュメントを検索する

find はカーソルを返します。findOne は単一のドキュメントを返します。

▶ 例:商品の検索

JAVASCRIPT
const product = await products.findOne({ category: 'electronics' });
console.log(product);

const cursor = products.find({ price: { $gt: 50 } });
const expensive = await cursor.toArray();
console.log(`${expensive.length} products found`);

(3) ドキュメントを更新する

updateOne は最初のマッチを更新し、updateMany はすべてのマッチを更新します。

▶ 例:商品の価格を更新する

JAVASCRIPT
const updateResult = await products.updateOne(
  { name: 'Mechanical Keyboard' },
  { $set: { price: 79.99, updatedAt: new Date() } },
);

console.log('Modified count:', updateResult.modifiedCount);

(4) ドキュメントを削除する

deleteOne は最初の一致を削除し、deleteMany はすべての一致を削除します。

▶ 例:商品の削除

JAVASCRIPT
const deleteResult = await products.deleteOne({
  name: 'Mechanical Keyboard',
});

console.log('Deleted count:', deleteResult.deletedCount);

(5) CRUDメソッドのクイックリファレンス

操作 方法 戻り値 説明
1件のレコードを挿入 insertOne(doc) {insertedId} 自動生成されたIDを返す
複数挿入 insertMany([doc]) {insertedIds, insertedCount} 一括挿入
1件のレコードを検索 findOne(filter) ドキュメントまたはnull 最初の一致を返す
複数行のクエリ find(filter) カーソル toArray() または反復処理が必要
1件のデータを更新 updateOne(filter, update) {modifiedCount} 最初の1件のみを更新
複数のエントリを更新 updateMany(filter, update) {modifiedCount} すべての一致を更新
1件を削除 deleteOne(filter) {deletedCount} 最初の一致を削除
複数削除 deleteMany(filter) {deletedCount} 一致するものをすべて削除
文書内で置換 replaceOne(filter, doc) {modifiedCount} 文書全体で置換

4. ObjectId およびクエリ演算子

(1) ObjectId メカニズム

各ドキュメントのデフォルト値 _idObjectId 型であり、この 12 バイトのエンコーディングには、タイムスタンプ、マシン識別子、およびカウンタが含まれています。

▶ 例:ObjectId の使用

JAVASCRIPT
const { ObjectId } = require('mongodb');

const id = new ObjectId();
console.log('ID string:', id.toHexString());
console.log('Timestamp:', id.getTimestamp());

const product = await products.findOne({
  _id: new ObjectId('6850a1b2c3d4e5f6a7b8c9d0'),
});

(2) 比較演算子

▶ 例:比較クエリ

JAVASCRIPT
const expensive = await products.find({ price: { $gt: 100 } }).toArray();
const cheap = await products.find({ price: { $lt: 20 } }).toArray();
const midRange = await products.find({ price: { $gte: 50, $lte: 100 } }).toArray();

(3) 論理演算子および集合演算子

▶ 例:$in および $or クエリ

JAVASCRIPT
const selected = await products.find({
  category: { $in: ['electronics', 'books'] },
}).toArray();

const mixed = await products.find({
  $or: [
    { price: { $lt: 10 } },
    { category: 'electronics' },
  ],
}).toArray();

(4) 正規表現による検索

▶ 例:商品名の正規表現による一致

JAVASCRIPT
const matched = await products.find({
  name: { $regex: /^Mechanical/i },
}).toArray();

(5) クエリ演算子のクイックリファレンス

演算子 構文 説明
$eq {field: {$eq: val}} これと同じ(および {field: val}
$gt {field: {$gt: val}} より大きい
$gte {field: {$gte: val}} 以上
$lt {field: {$lt: val}} 未満
$lte {field: {$lte: val}} 以下
$ne {field: {$ne: val}} 異なる
$in {field: {$in: [v1,v2]}} 配列内
$nin {field: {$nin: [v1,v2]}} 配列に含まれていない
$or {$or: [{...},{...}]} または条件
$and {$and: [{...},{...}]} 条件
$not {field: {$not: {...}}} 選択解除
$regex {field: {$regex: 'pattern'}} 正規表現の一致
$exists {field: {$exists: true}} そのフィールドは存在しますか?

5. 投影と並べ替え

(1) 投影制御リターンフィールド

プロジェクションでは、返すフィールドと除外するフィールドを指定することで、ネットワークトラフィックを削減します。

▶ 例:投影クエリ

JAVASCRIPT
const names = await products.find(
  {},
  { projection: { name: 1, price: 1, _id: 0 } },
).toArray();

const withoutSpecs = await products.find(
  {},
  { projection: { specs: 0, createdAt: 0 } },
).toArray();

(2) 並べ替えとページネーション

sort は検索結果を並べ替え、skiplimit はページネーション機能を実装しています。

▶ 例:ソートとページネーション

JAVASCRIPT
const page = 2;
const pageSize = 10;

const sorted = await products.find({})
  .sort({ price: -1, name: 1 })
  .skip((page - 1) * pageSize)
  .limit(pageSize)
  .toArray();

6. インデックスの基礎

(1) インデックスを作成する

インデックスはクエリの実行を高速化しますが、書き込みのオーバーヘッドとストレージ容量が増加します。

▶ 例:インデックスの作成

JAVASCRIPT
await products.createIndex({ name: 1 });
await products.createIndex({ category: 1, price: -1 });
await products.createIndex({ name: 'text' });

const indexes = await products.indexes();
console.log(indexes);

(2) 一意インデックスと複合インデックス

▶ 例:一意インデックス

JAVASCRIPT
await products.createIndex({ sku: 1 }, { unique: true });

7. MongoDB の接続と操作手順

100%
flowchart TD
    A[App Launch] --> B[Create MongoClient]
    B --> C[client.connect]
    C -->|Success| D[Get db Examples]
    C -->|Failure| E[Error Handling/Retry]
    E --> C
    D --> F[Get collection]
    F --> G{CRUD Operation}
    G -->|Write| H[insertOne / insertMany]
    G -->|Read| I[find / findOne]
    G -->|Update| J[updateOne / updateMany]
    G -->|Delete| K[deleteOne / deleteMany]
    H --> L[Back insertedId]
    I --> M[Back to the Document/Cursor]
    J --> N[Back modifiedCount]
    K --> O[Back deletedCount]
    L --> P{Continue?}
    M --> P
    N --> P
    O --> P
    P -->|Yes| G
    P -->|No| Q[client.close]
    Q --> R[Exit the app]

8. 包括的な例:プロダクトマネジメントのデータアクセス層

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

class ProductRepository {
  constructor(uri, dbName) {
    this.client = new MongoClient(uri, {
      maxPoolSize: 10,
      serverSelectionTimeoutMS: 5000,
    });
    this.dbName = dbName;
    this.collection = null;
  }

  async connect() {
    await this.client.connect();
    const db = this.client.db(this.dbName);
    this.collection = db.collection('products');
    await this.collection.createIndex({ name: 1 });
    await this.collection.createIndex({ category: 1, price: -1 });
    console.log('ProductRepository connected');
  }

  async create(productData) {
    const doc = {
      ...productData,
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    const result = await this.collection.insertOne(doc);
    return { ...doc, _id: result.insertedId };
  }

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

  async findByCategory(category, page = 1, pageSize = 10) {
    const skip = (page - 1) * pageSize;
    const [items, total] = await Promise.all([
      this.collection.find({ category })
        .sort({ price: -1 })
        .skip(skip)
        .limit(pageSize)
        .project({ name: 1, price: 1, category: 1 })
        .toArray(),
      this.collection.countDocuments({ category }),
    ]);
    return { items, total, page, pageSize };
  }

  async update(id, updates) {
    const result = await this.collection.updateOne(
      { _id: new ObjectId(id) },
      { $set: { ...updates, 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 disconnect() {
    await this.client.close();
    console.log('ProductRepository disconnected');
  }
}

async function main() {
  const repo = new ProductRepository(
    'mongodb://localhost:27017',
    'ecommerce',
  );
  try {
    await repo.connect();
    const created = await repo.create({
      name: 'Wireless Mouse',
      price: 29.99,
      category: 'electronics',
      specs: { dpi: 16000, buttons: 6 },
    });
    console.log('Created:', created._id);
    const found = await repo.findById(created._id);
    console.log('Found:', found.name);
    await repo.update(created._id, { price: 24.99 });
    const page = await repo.findByCategory('electronics', 1, 10);
    console.log('Page items:', page.items.length);
    await repo.delete(created._id);
    console.log('Deleted');
  } finally {
    await repo.disconnect();
  }
}

main().catch(console.error);

❓ よくある質問

Q なぜMySQLではなくMongoDBを使うのですか?
A データ構造が頻繁に変化し、フィールドが固定されていない場合、MongoDBのドキュメントモデルならテーブルの変更を必要とせずに柔軟に対応できます。一方、トランザクション数が多く、複雑な関係性が関わる業務の場合は、MySQLの方が適しています。
Q ObjectId とは何ですか?
A ObjectId は、MongoDB によって自動的に生成される 12 バイトの一意の識別子です。最初の 4 バイトはタイムスタンプであり、id.getTimestamp() を使用すれば、追加のフィールドを指定することなく作成時刻を取得できます。
Q 接続プールのサイズはどのように設定すべきですか?
A 一般的には、CPUコア数の5~10倍に設定します(maxPoolSize)。I/O負荷の高いアプリケーションの場合は、適宜増やすことができます。コールドスタートの遅延を避けるため、minPoolSizeは2~5に設定してください。
Q 接続エラーにはどのように対処すればよいですか?
A serverSelectionTimeoutMS パラメータを設定して待機時間を制限し、catch ブロックでエラーをログに記録し、グレースフル・デグラデーションを実装してください。本番環境では、リトライ機構とヘルスチェックの使用を推奨します。
Q MongoDBはどのようなシナリオに適していますか?
A コンテンツ管理、ログ分析、IoTデータ、製品カタログなど、柔軟なスキーマと頻繁な読み書き操作が求められるシナリオに適しています。一方、強力なトランザクションの一貫性が求められる中核的な金融システムには適していません。
Q 投影において、1と0を混在させることはできますか?
A _id を除き、混在させることはできません。指定されたフィールドを含める場合はすべて 1 を、除外する場合はすべて 0 を使用してください。_id はデフォルトで返されますが、個別に 0 に設定することで除外することができます。

9. 演習問題

  1. ローカルの MongoDB インスタンスに接続し、異なるカテゴリに属する 5 つの商品ドキュメントを挿入するスクリプトを作成してください。各ドキュメントには、少なくとも 3 つの異なるフィールドが含まれている必要があります。
  2. 価格範囲($gte/$lte)によるクエリを実行し、結果を価格の降順で並べ替え、「name」および「price」フィールドのみを返す
  3. $in$regex を組み合わせて、カテゴリが指定されたリストに含まれ、商品名に特定のキーワードが含まれる商品を検索します。
  4. 頻繁にクエリされるフィールドに対して複合インデックスを作成し、explain() を使用して、インデックス作成前後の実行プランの違いを比較します。
  5. filter、projection、sort、page、pageSize の各パラメータを受け取る、汎用的なページネーション付きクエリ関数を作成する

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%