Menu
CoddyTech

Index uniques

Fait partie de la section Bases de données avec MongoDB du Journey Node.js de Coddy. Leçon 17 sur 29.

Deux livres ayant le même ISBN ne devraient jamais être stockés tous les deux, et un findOne avant chaque insertion laisse une faille : deux requêtes peuvent ne rien trouver et toutes les deux insérer le livre.

Un index est une liste triée des valeurs d'un champ que la base de données conserve à côté d'une collection pour trouver rapidement des documents. Un index unique refuse également un second document ayant une valeur déjà présente : l'insertion lève une MongoServerError avec le code 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;
    // l’adresse e-mail est déjà utilisée
}

Crée l'index au démarrage, avant d'insérer les données. La création d'un index qui existe déjà ne fait rien.

challenge icon

Défi

Facile

Aucun livre ne peut avoir le même ISBN qu’un autre, même si deux requêtes arrivent au même instant. Laisse la base de données refuser le doublon.

  1. Dans server.js, en haut du fichier, crée un index unique sur isbn. Fais-le avant que le code de démarrage stocke les trois livres avec insertMany, et attends sa fin avec await.
  2. Dans POST /books, l’insertion déclenche désormais une erreur si l’ISBN est déjà enregistré. Intercepte cette erreur. Lorsque son code vaut 11000, renvoie le statut 409 et une erreur, par exemple { "error": "That ISBN is already on the shelf" }. Relance toute autre erreur afin qu’Express renvoie une erreur 500.
  3. Un livre avec un nouvel ISBN reçoit toujours le statut 201 et le livre enregistré, comme avant. Essaie : Envoie POST /books avec un deuxième exemplaire de Dune : { "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi" }. Tu obtiens le statut 409 et l’erreur. Envoie GET /books : tu obtiens toujours trois livres, et l’onglet Data affiche Dune une seule fois. Envoie POST /books avec { "title": "Beloved", "author": "Toni Morrison", "year": 1987, "isbn": "9781400033416", "genre": "literary" } : tu obtiens le statut 201.

Essayez vous-même

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());

// Chaque erreur renvoie { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Le :id sous forme d’ObjectId, ou null après avoir renvoyé 400 si l’identifiant est invalide.
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 iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Bases de données avec MongoDB

Entraînez-vous par vous-même : Playground Express