Уникальные индексы
Часть раздела Базы данных с MongoDB путешествия по Node.js на Coddy. Урок 17 из 29.
Две книги с одним ISBN никогда не должны храниться одновременно, а findOne перед каждой вставкой оставляет лазейку: два запроса могут оба ничего не найти и оба выполнить вставку.
Индекс — это отсортированный список значений поля, который база данных хранит рядом с коллекцией, чтобы быстро находить документы. Уникальный индекс также запрещает добавлять второй документ со значением, которое в нём уже есть: вставка вызывает MongoServerError с кодом 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;
// этот адрес электронной почты уже занят
}Создайте индекс при запуске, до добавления данных. Создание уже существующего индекса ничего не делает.
Задание
ЛегкоУ двух книг не может быть одинакового ISBN, даже если два запроса поступают одновременно. Пусть база данных отклоняет дубликат.
- В
server.js, в самом начале, создайте уникальный индекс дляisbn. Сделайте это до того, как начальный код сохранит три книги с помощьюinsertMany, и дождитесь его выполнения с помощьюawait. - В
POST /booksпри попытке вставки теперь возникает ошибка, если ISBN уже сохранён. Перехватите эту ошибку. Если еёcodeравен11000, верните статус 409 и ошибку, например{ "error": "That ISBN is already on the shelf" }. Любую другую ошибку выбросьте снова, чтобы Express вернул статус 500. - Книга с новым ISBN по-прежнему получает статус 201 и сохранённую книгу, как и раньше. Попробуйте: отправьте
POST /booksсо второй копией Dune:{ "title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593", "genre": "scifi" }. Вы получите статус 409 и ошибку. ОтправьтеGET /books: вы по-прежнему получите три книги, а на вкладке Data Dune отобразится один раз. ОтправьтеPOST /booksс{ "title": "Beloved", "author": "Toni Morrison", "year": 1987, "isbn": "9781400033416", "genre": "literary" }: вы получите статус 201.
Попробуйте сами
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());
// При любой ошибке возвращается ответ { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// Возвращает :id как ObjectId или null после ответа с кодом 400 при некорректном id.
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');
});
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Базы данных с MongoDB
2Изменение документов
Обновление документовОператоры обновленияПотренируйтесь самостоятельно: Песочница Express