Menu
CoddyTech

Índices únicos

Parte de la sección Bases de datos con MongoDB del Journey de Node.js de Coddy. Lección 17 de 29.

Nunca se deben almacenar dos libros con el mismo ISBN, y hacer un findOne antes de cada inserción deja una brecha: dos solicitudes pueden no encontrar nada y ambas insertar.

Un índice es una lista ordenada de los valores de un campo que la base de datos mantiene junto a una colección para encontrar documentos rápidamente. Un índice único también rechaza un segundo documento con un valor que ya contiene: la inserción lanza un MongoServerError con el código 11000, "E11000 duplicate key error".

await users.createIndex({ email: 1 }, { unique: true });

try {
    await users.insertOne({ email: 'ada@example.com' });
} catch (err) {
    if (err.code !== 11000) throw err;
    // el correo electrónico ya está en uso
}

Crea el índice al iniciar, antes de insertar los datos. Crear un índice que ya existe no hace nada.

challenge icon

Desafío

Fácil

Dos libros no pueden compartir un ISBN, incluso si dos solicitudes llegan al mismo tiempo. Deja que la base de datos rechace el duplicado.

  1. En server.js, al principio, crea un índice único en isbn. Hazlo antes de que el código inicial guarde los tres libros con insertMany y espera a que termine con await.
  2. En POST /books, la inserción ahora genera un error cuando el ISBN ya está guardado. Captura ese error. Cuando su code sea 11000, responde con el estado 409 y un error, por ejemplo { "error": "That ISBN is already on the shelf" }. Vuelve a generar cualquier otro error para que Express responda con 500.
  3. Un libro con un ISBN nuevo sigue recibiendo el estado 201 y el libro guardado, como antes. Pruébalo: Envía POST /books con una segunda copia de Dune: { "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi" }. Obtienes el estado 409 y el error. Envía GET /books: sigues obteniendo tres libros, y la pestaña Data muestra Dune una vez. Envía POST /books con { "title": "Beloved", "author": "Toni Morrison", "year": 1987, "isbn": "9781400033416", "genre": "literary" }: obtienes el estado 201.

Pruébalo tú mismo

import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';

const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shelf');
const books = db.collection('books');

await books.insertMany([
    { title: 'Dune', author: 'Frank Herbert', year: 1965, isbn: '9780441013593', genre: 'scifi' },
    { title: 'Kindred', author: 'Octavia E. Butler', year: 1979, isbn: '9780807083697', genre: 'scifi' },
    { title: 'Emma', author: 'Jane Austen', year: 1815, isbn: '9780141439587', genre: 'classic' },
]);

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

// Cada error devuelve { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Devuelve :id como ObjectId, o null después de responder con 400 si id no es válido.
function parseId(req, res) {
    if (ObjectId.isValid(req.params.id)) {
        return new ObjectId(req.params.id);
    }
    sendError(res, 400, 'Not a valid id');
    return null;
}

app.get('/', (req, res) => {
    res.send('Bookshelf API');
});

app.get('/books', async (req, res) => {
    const filter = {};
    if (req.query.author) {
        filter.author = req.query.author;
    }
    if (req.query.from || req.query.to) {
        filter.year = {};
        if (req.query.from) {
            filter.year.$gte = Number(req.query.from);
        }
        if (req.query.to) {
            filter.year.$lte = Number(req.query.to);
        }
    }
    const list = await books.find(filter).toArray();
    res.json(list);
});

app.post('/books', async (req, res) => {
    const book = req.body;
    await books.insertOne(book);
    res.status(201).json(book);
});

app.get('/books/newest', async (req, res) => {
    const n = Number(req.query.n) || 3;
    const list = await books
        .find({}, { projection: { title: 1, year: 1, _id: 0 } })
        .sort({ year: -1 })
        .limit(n)
        .toArray();
    res.json(list);
});

app.get('/books/:id', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const book = await books.findOne({ _id: id });
    if (!book) {
        return sendError(res, 404, 'No such book');
    }
    res.json(book);
});

app.patch('/books/:id', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.updateOne({ _id: id }, { $set: req.body });
    if (result.matchedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.json(await books.findOne({ _id: id }));
});

app.post('/books/:id/copies', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.updateOne({ _id: id }, { $inc: { copies: 1 } });
    if (result.matchedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.json(await books.findOne({ _id: id }));
});

app.post('/books/:id/tags', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.updateOne({ _id: id }, { $push: { tags: req.body.tag } });
    if (result.matchedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.json(await books.findOne({ _id: id }));
});

app.delete('/books/:id', async (req, res) => {
    const id = parseId(req, res);
    if (!id) return;
    const result = await books.deleteOne({ _id: id });
    if (result.deletedCount === 0) {
        return sendError(res, 404, 'No such book');
    }
    res.status(204).end();
});

app.listen(3000, () => {
    console.log('Bookshelf is running on port 3000');
});
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Bases de datos con MongoDB

Practica por tu cuenta: Playground de Express