iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Documents, Collections, DBs

MongoDB stores BSON documents inside collections inside databases. Documents are flexible-schema JSON-with-types; collections are like tables; databases group collections. Indexing, replication, and sharding live on top.

Database, collection, doc, _id, indexes

EXAMPLE
// 1) Hierarchy
// cluster
//   └── database  (e.g. 'shop')
//         └── collection (e.g. 'orders')
//               └── document  { _id: ObjectId(...), … }

// 2) Documents — JSON shape, BSON storage
{
    _id: ObjectId('6543210...'),       // always 12 bytes — timestamp+random+counter
    customerId: ObjectId('64999...'),
    items: [
        { sku: 'SKU-101', qty: 2, priceCents: 4999 },
        { sku: 'SKU-203', qty: 1, priceCents: 1299 },
    ],
    totalCents: 11297,
    status: 'paid',
    shippingAddress: { line1: '1 Pitt St', city: 'Sydney', country: 'AU' },
    createdAt: ISODate('2024-08-01T03:21:00Z'),
}

// 3) _id — the primary key
//   • Auto-generated as ObjectId if you don't supply one
//   • Indexed automatically
//   • Can be anything serializable: string, integer, UUID — but pick one shape
//   • ObjectId encodes timestamp → sort by _id == sort by creation time

import { ObjectId } from 'mongodb';
const id = new ObjectId();
id.getTimestamp();   // when this id was generated

// 4) Connect (Node driver)
import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.MONGO_URL);
await client.connect();
const db = client.db('shop');
const orders = db.collection('orders');

// 5) CRUD
await orders.insertOne({ totalCents: 4999, status: 'pending', createdAt: new Date() });
await orders.insertMany([{ … }, { … }]);

await orders.findOne({ _id: new ObjectId(id) });
await orders.find({ status: 'paid' }).sort({ createdAt: -1 }).limit(20).toArray();

await orders.updateOne(
    { _id: id },
    { $set: { status: 'shipped', shippedAt: new Date() } },
);
await orders.deleteOne({ _id: id });

// 6) Schema flexibility — both freedom and footgun
// MongoDB does not enforce a schema by default. Use validator OR validation library:

await db.createCollection('orders', {
    validator: {
        $jsonSchema: {
            bsonType: 'object',
            required: ['totalCents', 'status', 'createdAt'],
            properties: {
                totalCents: { bsonType: 'int', minimum: 0 },
                status:     { enum: ['pending', 'paid', 'shipped', 'cancelled'] },
                createdAt:  { bsonType: 'date' },
            },
        },
    },
    validationLevel:  'strict',
    validationAction: 'error',
});

// In app code: Zod, Mongoose, or Prisma for schema enforcement.

// 7) Indexes — make queries fast
await orders.createIndex({ customerId: 1 });
await orders.createIndex({ status: 1, createdAt: -1 });
await orders.createIndex({ 'shippingAddress.country': 1 });
await orders.createIndex({ items: 'multikey' });            // arrays get multikey indexes
await orders.createIndex({ description: 'text' });          // text search

// Compound index order matters! Sort matches the index direction or it's a scan.
await orders.find({ status: 'paid' }).sort({ createdAt: -1 });  // uses { status:1, createdAt:-1 }

// 8) Embed vs reference
// Embed when:
//   • child entities don't exist independently (order line items)
//   • you always read parent + children together
//   • child count is bounded
// Reference (separate collection) when:
//   • children are queried independently
//   • cardinality is high or unbounded (comments on a popular post)
//   • children have rich queries of their own

// 9) Aggregation pipeline
await orders.aggregate([
    { $match: { status: 'paid', createdAt: { $gte: ISODate('2024-01-01') } } },
    { $group: { _id: '$customerId', revenueCents: { $sum: '$totalCents' }, n: { $sum: 1 } } },
    { $sort:  { revenueCents: -1 } },
    { $limit: 10 },
]).toArray();

// 10) Transactions — multi-document atomicity
const session = client.startSession();
try {
    session.startTransaction();
    await orders.updateOne({ _id }, { $set: { status: 'paid' } }, { session });
    await ledger.insertOne({ orderId: _id, amountCents }, { session });
    await session.commitTransaction();
} catch (e) {
    await session.abortTransaction();
    throw e;
} finally {
    await session.endSession();
}
// Transactions require a replica set (any modern cluster, Atlas, or local rs).

// 11) Replication + sharding
//   • Replica set: 3+ nodes, one primary, others secondaries → HA + read scaling
//   • Sharding: horizontal partitioning by a shard key → write scaling, huge data
//   Choose a shard key that distributes writes evenly and matches your query patterns.

// 12) Common bugs
//   • Storing dates as strings → can't range-query → ALWAYS new Date()
//   • Storing money as floats → ROUND ERRORS → store cents as integers (Int32/Int64)
//   • No index on common filter → collection scans → slow at scale
//   • _id mismatch — passing a string id where ObjectId is expected
//   • Unbounded array growth in a doc → 16 MB doc limit; reference instead
//   • Missing $ on operators — { status: 'paid' } vs { $match: { status: 'paid' } }

Why it matters

MongoDB’s schema flexibility is a feature for evolving products and a footgun for shipping prod. Enforce shape with a Zod / Mongoose layer in the app, store money as integer cents, dates as Date, and add an index for every filter that touches a real workload.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Hierarchy:
//   Cluster -> Database -> Collection -> Document
Try it Yourself »

Discussion

Loading…