「第21課:MongoDBの基礎」
1. なぜMongoDBを選ぶのか
AliceのeコマースAPIは、厄介な問題に直面しています。それは、商品属性がカテゴリーごとに大きく異なるという点です。たとえば、ノートパソコンにはCPUやメモリのフィールドがあり、衣類にはサイズや色のフィールドがあり、食品には賞味期限のフィールドがあります。リレーショナルデータベースを使用する場合、新しいカテゴリーが追加されるたびにテーブル構造を変更する必要があります。一方、MongoDBのドキュメントモデルは、構造の異なるレコードを自然にサポートしています。各商品ドキュメントは、スキーマの移行を一切必要とせずに、まったく異なるフィールドを持つことができます。
- MongoDB は、ドキュメントベースのデータベースであり、データを BSON 形式で格納します。BSON は JSON に似ていますが、より多くのデータ型をサポートしています。
- このドキュメントモデルは Node.js のオブジェクトモデルと自然に整合するため、ORM 変換にかかるオーバーヘッドを軽減します
- 固定のスキーマはなく、ビジネスの変化に応じてフィールドを動的に追加・削除できる
- 優れた水平スケーラビリティを備え、高同時アクセス環境での読み取りおよび書き込みに適している
- 複雑な検索要件に対応するための、幅広いクエリ演算子とインデックス作成メカニズム
| SQLの基礎 | MongoDBの基礎 | 説明 |
|---|---|---|
| データベース | データベース | データベース(用語の統一) |
| テーブル | コレクション | テーブル → コレクション |
| 行 | ドキュメント | 行 → ドキュメント |
| 列 | フィールド | 列 → フィールド |
| 主キー | _id (ObjectId) |
自動生成される主キー |
| JOIN | $lookup (集計) |
さまざまな種類の結合クエリ |
| スキーマ | スキーマの強制なし | オプションの検証ルール |
| インデックス | インデックス | 類似のインデックス作成メカニズム |
2. 設置と接続
(1) MongoDB ドライバーをインストールする
MongoDB サーバーに接続するには、公式の mongodb npm パッケージを使用してください。
▶ 例:ドライバのインストール
npm install mongodb
(2) クライアントとの接続を確立する
MongoClient は、MongoDB に接続するためのエントリポイントです。接続文字列を通じて、アドレスやオプションを指定します。
▶ 例:基本的な接続
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 接続プールが組み込まれています。プールのサイズと動作はオプションで制御されます。
▶ 例:コネクションプールの設定を用いた接続
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) 接続を適切に閉じる
アプリケーションが終了する際には、接続を閉じ、リソースを解放する必要があります。
▶ 例:正常なシャットダウン
process.on('SIGINT', async () => {
await client.close();
console.log('MongoDB connection closed');
process.exit(0);
});
3. CRUD操作
(1) 文書を挿入する
insertOne を使用すると単一の文書を挿入でき、insertMany を使用すると複数の文書を挿入できます。
▶ 例:製品ドキュメントの挿入
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 は単一のドキュメントを返します。
▶ 例:商品の検索
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 はすべてのマッチを更新します。
▶ 例:商品の価格を更新する
const updateResult = await products.updateOne(
{ name: 'Mechanical Keyboard' },
{ $set: { price: 79.99, updatedAt: new Date() } },
);
console.log('Modified count:', updateResult.modifiedCount);
(4) ドキュメントを削除する
deleteOne は最初の一致を削除し、deleteMany はすべての一致を削除します。
▶ 例:商品の削除
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 メカニズム
各ドキュメントのデフォルト値 _id は ObjectId 型であり、この 12 バイトのエンコーディングには、タイムスタンプ、マシン識別子、およびカウンタが含まれています。
▶ 例:ObjectId の使用
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) 比較演算子
▶ 例:比較クエリ
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 クエリ
const selected = await products.find({
category: { $in: ['electronics', 'books'] },
}).toArray();
const mixed = await products.find({
$or: [
{ price: { $lt: 10 } },
{ category: 'electronics' },
],
}).toArray();
(4) 正規表現による検索
▶ 例:商品名の正規表現による一致
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) 投影制御リターンフィールド
プロジェクションでは、返すフィールドと除外するフィールドを指定することで、ネットワークトラフィックを削減します。
▶ 例:投影クエリ
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 は検索結果を並べ替え、skip と limit はページネーション機能を実装しています。
▶ 例:ソートとページネーション
const page = 2;
const pageSize = 10;
const sorted = await products.find({})
.sort({ price: -1, name: 1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
.toArray();
1は昇順を示し、-1は降順を示します- 複数のフィールドによる並べ替えは、それらの宣言順に従って適用されます
skipレコード数が多すぎるとパフォーマンスが低下します。大規模なデータセットの場合は、代わりに範囲クエリを使用することをお勧めします。
6. インデックスの基礎
(1) インデックスを作成する
インデックスはクエリの実行を高速化しますが、書き込みのオーバーヘッドとストレージ容量が増加します。
▶ 例:インデックスの作成
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) 一意インデックスと複合インデックス
▶ 例:一意インデックス
await products.createIndex({ sku: 1 }, { unique: true });
- MongoDB は
_idに対して一意のインデックスを自動的に作成します - 複合インデックスは、最左前置子原則に従います
- テキストインデックスは全文検索に対応しています。コレクションごとに1つだけ設定可能です。
7. MongoDB の接続と操作手順
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. 包括的な例:プロダクトマネジメントのデータアクセス層
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);
❓ よくある質問
id.getTimestamp() を使用すれば、追加のフィールドを指定することなく作成時刻を取得できます。serverSelectionTimeoutMS パラメータを設定して待機時間を制限し、catch ブロックでエラーをログに記録し、グレースフル・デグラデーションを実装してください。本番環境では、リトライ機構とヘルスチェックの使用を推奨します。_id を除き、混在させることはできません。指定されたフィールドを含める場合はすべて 1 を、除外する場合はすべて 0 を使用してください。_id はデフォルトで返されますが、個別に 0 に設定することで除外することができます。9. 演習問題
- ローカルの MongoDB インスタンスに接続し、異なるカテゴリに属する 5 つの商品ドキュメントを挿入するスクリプトを作成してください。各ドキュメントには、少なくとも 3 つの異なるフィールドが含まれている必要があります。
- 価格範囲($gte/$lte)によるクエリを実行し、結果を価格の降順で並べ替え、「name」および「price」フィールドのみを返す
$inと$regexを組み合わせて、カテゴリが指定されたリストに含まれ、商品名に特定のキーワードが含まれる商品を検索します。- 頻繁にクエリされるフィールドに対して複合インデックスを作成し、
explain()を使用して、インデックス作成前後の実行プランの違いを比較します。 - filter、projection、sort、page、pageSize の各パラメータを受け取る、汎用的なページネーション付きクエリ関数を作成する
📖 まとめ
- 1 MongoDBを選ぶ理由:基本概念と使い方
- 2 設置と接続に関する主要な概念と使用方法
- 3 CRUD操作の基本概念と使い方
- 4 基本概念および ObjectId とクエリ演算子の使い方
- 5 投影とソートの基本概念と使い方
- インデックス作成の基礎:6つの基本概念と活用法
- MongoDBの接続と操作に関する7つの基本概念と使い方
- 8 包括的な事例:プロダクトマネジメントのデータアクセス層の核心となる概念と活用方法
📝 練習問題
- このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
- 包括的な例を修正し、独自の拡張機能を追加する
- 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
- 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
- このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。



