Начнём мы с изучения основных механизмов работы с MongoDB. Это самое основное, что понадобится для понимания MongoDB, но также мы коснёмся высокоуровневых вопросов — о том, где применима MongoDB.
Для начала нужно понять шесть основных концепций.
-
Один сервер или кластер MongoDB может содержать несколько баз данных. База данных — контейнер для коллекций; сам продукт MongoDB и отдельная база не являются одним и тем же.
-
В базе данных находятся коллекции. Их удобно сравнивать с таблицами, но документная модель и её ограничения отличаются от реляционной.
-
Коллекции состоят из нуля или более «документов». Опять же, документ можно рассматривать как «строку».
-
Документ состоит из одного или более «полей», которые — как можно догадаться — подобны «колонкам».
-
«Индексы» в MongoDB почти идентичны таковым в реляционных базах данных.
-
Курсор представляет результат запроса и получает документы порциями по мере чтения. Подсчёт, пропуск и сортировка могут потребовать работы сервера: наличие курсора не делает эти операции бесплатными.
Подводя итог, MongoDB состоит из «баз данных», которые состоят из «коллекций». «Коллекции» состоят из «документов». Каждый «документ» состоит из «полей». «Коллекции» могут быть проиндексированы, что улучшает производительность выборки и сортировки. И наконец, получение данных из MongoDB сводится к получению «курсора», который отдаёт эти данные по мере надобности.
Вы можете спросить — зачем придумывать новые термины (коллекция вместо таблицы, документ вместо записи и поле вместо колонки)? Не излишнее ли это усложнение? Ответ в том, что эти термины, хоть и близки своим «реляционным» аналогам, но не полностью идентичны им. Основное различие в том, что реляционные базы данных определяют «колонки» на уровне «таблицы», в то время как документ-ориентированные базы данных определяют «поля» на уровне «документа». Это значит, что любой документ внутри коллекции может иметь свой собственный уникальный набор полей. В этом смысле коллекция «глупее» чем таблица, тогда как документ имеет намного больше информации, чем строка.
Документы одной коллекции могут иметь разный набор полей. Это не означает отсутствия правил: MongoDB поддерживает валидацию схемы, а приложение всё равно должно проверять данные. В учебных примерах схема задаётся соглашением между вставками и запросами.
Приступим. Запустите сервер mongod и оболочку mongosh, если ещё не запустили. Консоль работает на JavaScript. Есть несколько глобальных команд, например help или exit. Команды, которые вы запускаете применительно к текущей базе данных исполняются у объекта db, например db.help() или db.stats() . Команды, которые вы запускаете применительно к конкретной коллекции, исполняются у объекта db.ИМЯ_КОЛЛЕКЦИИ, например db.unicorns.help() или db.unicorns.countDocuments({}) .
Введите db.help() и получите список команд, которые можно выполнить у объекта db.
Заметка на полях. Поскольку консоль интерпретирует JavaScript, если вы попытаетесь выполнить метод без скобок, то в ответ получите тело метода, но он не выполнится. Не удивляйтесь, увидев function (...){, если случайно сделаете так. Например, если введёте db.help (без скобок), вы увидите внутреннее представление метода help.
Сперва для выбора базы данных воспользуемся глобальным методом use — введите use learn. Неважно, что база данных пока ещё не существует. В момент создания первой коллекции создастся база данных learn. Теперь, когда вы внутри базы данных, можно вызывать у неё команды, например db.getCollectionNames(). В ответ увидите пустой массив ([ ]). Поскольку коллекции бесструктурны (в оригинале «schema-less». Здесь и далее — прим. перев.), мы не обязаны создавать их явно. Мы просто можем вставить документ в новую коллекцию. Чтобы это сделать, используйте команду insert, передав ей вставляемый документ:
db.unicorns.insertOne({name: 'Aurora', gender: 'f', weight: 450})
Строка вызывает insertOne для коллекции unicorns. Внутри MongoDB хранит BSON — бинарное представление документов с дополнительными типами, включая ObjectId и даты. Записанный в оболочке JavaScript-объект не обязан быть строгим JSON. Для просмотра коллекций выполните db.getCollectionNames(), для индексов конкретной коллекции — db.unicorns.getIndexes().
Теперь у коллекции unicorns можно вызвать метод find, который вернёт список документов:
db.unicorns.find()
Если поле _id не задано, для обычной коллекции MongoDB добавит уникальный идентификатор. В этих примерах это ObjectId. Поле _id индексируется; список индексов смотрите так:
db.unicorns.getIndexes()
Вы увидите имя индекса, базы данных и коллекции, для которой индекс был создан, а также полей, которые включены в него.
Вернёмся к обсуждению бесструктурных коллекций. Давайте вставим кардинально отличный от предыдущего документ в unicorns, вот такой:
db.unicorns.insertOne({name: 'Leto', gender: 'm', home: 'Arrakeen', worm: false})
И снова воспользуемся find для просмотра списка документов. Теперь, узнав чуть больше, мы можем обсудить это интересное поведение MongoDB, но, надеюсь, вы уже начинаете понимать, почему традиционная терминология здесь не совсем применима.
Осваиваем селекторы
Фильтр запроса задаётся документом: в mongosh он записывается как JavaScript-объект и передаётся серверу в BSON. Это не обязательно строгий JSON. Пустой фильтр {} выбирает все документы. Не заменяйте его null: используйте явный объект. Например, {gender: "f"} выбирает документы с указанным значением поля.
Перед следующим набором данных очистите только учебную коллекцию: проверьте db.getName() и убедитесь, что это learn, затем выполните db.unicorns.deleteMany({}). Пустой фильтр удалит все документы этой коллекции. Не выполняйте эту команду в рабочей базе. Теперь вставьте демонстрационные документы:
db.unicorns.insertOne({name: 'Horny', dob: new Date(1992,2,13,7,47), loves: ['carrot','papaya'], weight: 600, gender: 'm', vampires: 63});
db.unicorns.insertOne({name: 'Aurora', dob: new Date(1991, 0, 24, 13, 0), loves: ['carrot', 'grape'], weight: 450, gender: 'f', vampires: 43});
db.unicorns.insertOne({name: 'Unicrom', dob: new Date(1973, 1, 9, 22, 10), loves: ['energon', 'redbull'], weight: 984, gender: 'm', vampires: 182});
db.unicorns.insertOne({name: 'Roooooodles', dob: new Date(1979, 7, 18, 18, 44), loves: ['apple'], weight: 575, gender: 'm', vampires: 99});
db.unicorns.insertOne({name: 'Solnara', dob: new Date(1985, 6, 4, 2, 1), loves:['apple', 'carrot', 'chocolate'], weight:550, gender:'f', vampires:80});
db.unicorns.insertOne({name:'Ayna', dob: new Date(1998, 2, 7, 8, 30), loves: ['strawberry', 'lemon'], weight: 733, gender: 'f', vampires: 40});
db.unicorns.insertOne({name:'Kenny', dob: new Date(1997, 6, 1, 10, 42), loves: ['grape', 'lemon'], weight: 690, gender: 'm', vampires: 39});
db.unicorns.insertOne({name: 'Raleigh', dob: new Date(2005, 4, 3, 0, 57), loves: ['apple', 'sugar'], weight: 421, gender: 'm', vampires: 2});
db.unicorns.insertOne({name: 'Leia', dob: new Date(2001, 9, 8, 14, 53), loves: ['apple', 'watermelon'], weight: 601, gender: 'f', vampires: 33});
db.unicorns.insertOne({name: 'Pilot', dob: new Date(1997, 2, 1, 5, 3), loves: ['apple', 'watermelon'], weight: 650, gender: 'm', vampires: 54});
db.unicorns.insertOne({name: 'Nimue', dob: new Date(1999, 11, 20, 16, 15), loves: ['grape', 'carrot'], weight: 540, gender: 'f'});
db.unicorns.insertOne({name: 'Dunx', dob: new Date(1976, 6, 18, 18, 18), loves: ['grape', 'watermelon'], weight: 704, gender: 'm', vampires: 165});
Теперь, когда данные созданы, можно приступать к освоению селекторов. {поле: значение} используется для поиска всех документов, у которых поле равно значение. {поле1: значение1, поле2: значение2} работает как логическое И. Специальные операторы $lt, $lte, $gt, $gte и $ne используются для выражения операций «меньше», «меньше или равно», «больше», «больше или равно», и «не равно». Например, чтобы получить всех самцов единорога, весящих более 700 фунтов, мы можем написать:
db.unicorns.find({gender: 'm', weight: {$gt: 700}})
//или (что не полностью эквивалентно, но приведено здесь в демонстрационных целях)
db.unicorns.find({gender: {$ne: 'f'}, weight: {$gte: 701}})
Оператор $exists используется для проверки наличия или отсутствия поля, например:
db.unicorns.find({vampires: {$exists: false}})
Вернёт единственный документ. Если нужно ИЛИ вместо И, мы можем использовать оператор $or и присвоить ему массив значений, например:
db.unicorns.find({gender: 'f', $or: [{loves: 'apple'}, {loves: 'orange'}, {weight: {$lt: 500}}]})
Вышеуказанный запрос вернёт всех самок единорогов, которые или любят яблоки, или любят апельсины, или весят менее 500 фунтов.
В нашем последнем примере произошло кое-что интересное. Вы заметили — поле loves это массив. MongoDB поддерживает массивы как объекты первого класса. Это потрясающе удобная возможность. Начав это использовать, вы удивитесь, как вы раньше жили без этого. Самое интересное это та простота, с которой делается выборка по значению массива: {loves: 'watermelon'} вернёт нам все документы, у которых watermelon является одним из значений поля loves.
Для большинства фильтров достаточно обычных операторов сравнения и логических операторов. Если нужно сравнить вычисленные выражения, изучите $expr. Старое предложение использовать $where как универсальный гибкий фильтр исключено: оно исполняет JavaScript на сервере и не должно подставлять непроверенный пользовательский код. Предпочитайте обычные операторы запроса.
Фильтр используется не только в find, но и в deleteOne, deleteMany, countDocuments, updateOne и updateMany. Название метода определяет, читает ли он документы или изменяет их и сколько совпадений затрагивает.
Для воспроизводимого поиска возьмите _id реально существующего учебного документа. Так не требуется подставлять выдуманную строку идентификатора:
var sample = db.unicorns.findOne({name: "Aurora"});
if (sample) db.unicorns.find({_id: sample._id});
В этой главе
Мы рассмотрели документы, _id, insertOne, deleteMany и фильтры find. Следующая глава разделяет полную замену документа и изменение выбранных полей. Удаление всегда начинается с проверки базы и фильтра, а не с копирования команды из примера.