-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Модуль 5 «Базы данных» #5
Merged
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Начнём описывать спецификацию с заголовков. Воспользуемся форматом YAML и определим основные заголовки.
Начинать спецификацию лучше всего с определения сущностей. Они нам все известны: `offer`, `category`, `comment` и `user`. Все сущности перечислены в техническом задании. Определим сущности в виде тегов. Теги позволяют выполнить группировку действий в сгенерированном интерфейсе.
Пользуясь знаниями о проектировании API, попробуем определить ресурсы для сущности `user`. Мы можем оттолкнуться от действий, перечисленных в техническом задании.
При описании API мы можем детализировать запросы и ответы. То есть определять данные для отправки, а также ответы сервера. Вполне вероятно, что форма некоторых данных будет повторяться. Поэтому лучше воспользоваться компонентами: определить данные один раз и потом их использовать.
Во время разработки приходится часто перезапускать приложение. Например, после внесения очередной порции кода. Поправив что-то в исходном коде, нужно выполнить сценарий `npm run start:dev`, чтобы перезапустить приложение и протестировать изменения. Пока у нас не было базы данных, выполнение этой задачи мешало несильно. Приложение выполнялось и автоматически прекращало работу. По факту нам приходилось просто запускать его. Теперь, когда у нас есть постоянное соединение с базой данных, в будущем появится работающий сервер, приложение автоматически не завершается. Перед очередным запуском его нужно остановить, а потом вновь запустить. Это неудобно. Особенно, при интенсивном внесении изменений в код. Автоматизировать выполнение этой задачи поможет пакет `nodemon`. Про него мы рассказывали в учебнике. Он может следить за изменением файлов и при обнаружении изменений он автоматически выполнит перезапуск приложения. От нас требуется только сохранить изменения в файле. Стоит отметить, что `nodemon` следует применять только на этапе разработки. Поэтому пользоваться им стоит в сценарии `npm run start:dev`. Установим пакет в виде зависимости для разработки.
В самом простом случае достаточно прописать запуск `nodemon` в нужный сценарий запуска. Например, если мы хотим использовать `nodemon` для сценария `start:dev`, то прямо в нём можем воспользоваться `nodemon`. Такой пример доступен в документации к пакету. Помимо этого, у нас есть возможность сконфигурировать `nodemon`. Указать директорию, за изменением файлов в которой он должен наблюдать. Перечислить расширения таких файлов и указать команду, которую следует выполнять при изменении отслеживаемых файлов. Конфигурацию для `nodemon` можно описать в отдельном файле настроек. Создадим в корне проекта файл `nodemon.json` и определим конфигурацию. В секции `watch` укажем директорию, в которой будем отслеживать изменения файлов. Затем укажем расширения для отслеживаемых файлов. Нас интересуют только файлы с расширениями `*.ts` и `*.json`. Финальным штрихом зададим значение для директивы `exec`. В ней пропишем команду для перезапуска. Эту команду мы перенесём из сценария `start:dev`. В самом сценарии `start:dev` оставим вызов `nodemon`. Проделав перечисленные изменения, запустите приложение с помощью `npm run start:dev`. Приложение должно запуститься в обычном режиме, только в начале отобразится информация о запуске через `nodemon`. После этого попробуйте внести изменение в любой файл в директории `src` и сохранить изменения. Обратите внимание в терминал, `nodemon` автоматически выполнит перезапуск.
Техническое задание предусматривает получения списка категорий. Для этого потребуется в `DefaultCategoryService` метод для получения всех категорий. Расширим интерфейс `CategoryService` и добавим в него метод `find`. Он не принимает никаких аргументов. Его задача — вернуть все категории из базы данных. Затем откроем реализацию (`DefaultCategoryService`) и в нём имплементируем метод `find`.
Если сейчас воспользоваться `DefaultOfferService` и попробовать получить объявление по идентификатору, то вместо информации об авторе вы увидите идентификатор документа из коллекции `users`. Как правило, это не то что нам нужно, когда требуется предоставить полную информацию по объявлению клиенту. На месте `userId`, а также свойство `categories`. В первом должна быть информация о пользователе — авторе объявления, а во втором список категорий. Полная информация по этим сущностям хранится в отдельных коллекциях, а в `Offers` только идентификаторы. Клиенту обычно требуется полная информация. Например, вместо идентификатора пользователя, клиенту удобней получить объект с описанием пользователя (имя, фамилия, email и так далее). То же самое с категориями. Клиенту требуется полная информация по категориям, а не просто их идентификаторы. Как решить эту задачу и предоставить клиенту все необходимые данные? Можно самостоятельно выбрать все необходимые данные из базы и собрать объект нужной формы. Другой способ: воспользоваться методом `populate`, который предоставляет Mongoose. Обновим метод `findById` у `OfferService`. Добавим `populate`. Аргументами он принимает список полей, для которых следует выбрать данные из связанных коллекций. Нас интересуют категории (`Categories`) и пользователи (`Users`). После внесения изменений, сервис будет возвращать полную информацию по категориям и пользователю, включая служебные поля.
Мы реализовали базовые действия в сервисе для работы с объявлениями. Самое время заглянуть в технические задание и прикинуть, какие действия нам потребуются ещё: - Получение списка объявлений (`find`); - Удаление объявления по идентификатору (`deleteById`); - Обновление объявления (`updateById`); - Получение списка объявлений из определённой категории (`findByCategoryId`); - Получение списка новых объявлений (`findNew`); - Получение списка обсуждаемых объявлений (`findDiscussed`); - Обновление свойства «CommentsCount» (`incCommentCount`). Большинство методов не требуют отдельного пояснения. Их задачи предельно ясны. А вот метод `incCommentCount` заслуживает отдельного внимание. По задумке он должен обновлять количество комментариев к объявлению. Например: создали новый комментарий, обновили сразу количество. Это один из способов решения подобных задач. Другой — посчитать динамически. Мы его рассмотрим на другой похожей задачке.
В этом коммите реализуем все методы обновлённого интерфейса `OfferService`. Принцип реализации не сильно отличается: по факту это комбинация методов, которые предоставляет Mongoose. Здесь мы добавляем вызов метода `sort` (для сортировки), `findByIdAndUpdate` (обновления документа), `limit` (ограничения вывода) и так далее.
Реализуем поддержку комментариев. Начнём с описания сущности — `CommentEntity`. Комментарии будем хранить в отдельной коллекции `comments`. Мы конечно можем их сохранять в коллекции `Offers`, но это неоптимальное решение. Для одного объявления может быть множество комментариев. Извлекать их все, когда требуется только информация об объявлении — затратная операция. Поэтому хранить комментарии будем в отдельной коллекции. Каждый комментарий содержит минимальное количество информации: текст комментария, дату публикации (потребуется для сортировки). Также потребуется фиксировать автора комментария и объявление для которого этот комментарий был создан. Полная информация об объявлении и авторе хранится в отдельных коллекциях, поэтому можем указать ссылки на соответствующие сущности с помощью `Ref`. На уровне БД мы сохраним идентификаторы соответствующих сущностей — `OfferEntity` и `UserEntity`. Отдельное поле для хранения даты добавления комментария заводить не будем. Мы отнаследуемся от `TimeStamp`, поэтому дату добавления комментария сможем получить из поля `createdAt`.
Опираясь на описание сущности `CommentEntity` подготовим DTO `CreateCommentDto`. В нём определим три поля: `text`, `offerId` и `userId`. На самом деле нам потребуется только два поля. Идентификатор пользователя (автора комментария) мы можем определить автоматически на основании авторизации, но эту задачу реализуем немного позже.
Следующим шагом подготовим всё необходимое для сохранения данных в базу. Начнём с интерфейса `CommentService`. Сервис должен уметь сохранять данные в базу (метод `create`) и извлекать комментарии для определённого объявления (метод `findByOfferId`). Сервиса будет внедряться в виде зависимости, поэтому сразу добавим информацию о нём и модели в контейнер зависимостей. Для этого добавим новый компонент (`Component`) и соответствующие привязки в `main.rest.ts`.
Метод для обновления `UserEntity` в настоящее время нереализован, поэтому расширим интерфейс `UserService` и добавить метод `updateById`. Метод имплементируем по аналогии с аналогичным в `DefaultOfferService`. Для выполнения фактического обновления потребуется создать `UpdateUserDto`.
…e`. WIP MongoDB предоставляет всё необходимое для реализации сложных выборок — агрегацию. С помощью агрегации можно получать данные из других коллекций, трансформировать результат, выполнять сортировку, добавлять дополнительные поля и многое другое. Особенность агрегации заключается в том, что эти операции происходят на уровне БД, а не в приложении. Давайте подумаем, что требуется для подсчёта количества объявлений, которые соответствуют определённой категории. В первую очередь необходима информация о самих объявлениях. Информация об объявлениях находится в отдельной коллекции `Offers`. Соответствие между категориями и объявлением происходит через поле `categories` (массив с идентификаторами категориями). Чтобы посчитать количество объявлений, которые соответствуют категории, необходимо получить информацию по объявлениям. Чтобы включить документы из другой коллекции MongoDB предлагает оператор `$lookup`. С её помощью можно делать соединения: взять данные из одной коллекции и добавить их в результирующую выборку. Для работы `$lookup` требуется передать несколько аргументов (объект с параметрами). Сначала определим коллекцию откуда будем брать данные. Для этого определим ключ `from`. Нам потребуется информацию из коллекции `offers`. Как будем фильтровать объявления? По идентификатору категории. Поэтому объявим переменную (`let`) и сохраним идентификатор категории в `categoryId`. Позже эту и переменную сможем использовать в построении условия. Далее опишем конвейер (`pipeline`). В нём можно определить операции, которые необходимо выполнить над данными. Начнём с фильтрации. Для этого MongoDB предлагает оператор `$match`. В самых простых случаях ему можно передать значение по которому выполнять фильтрацию. Точно так как мы делали в `find`. Если требуется более сложное условие, можем воспользоваться дополнительными операторами и указать условие в выражении (`$expr`). В нём мы используем оператор `$in` и создаём условие: `$$categoryId` должна входить в массив `$categories`. Переменная `$$categoryId` — это переменная, которую мы определили в секции `let`. После фильтрации мы можем немного изменить форму данных. Нам не нужны абсолютно все поля из `OfferEntity`. Поэтому мы воспользуемся функцией `$project`. Из полей оставим только `_id`. Всё. Если сейчас выполнить получение списка категорий и вывести результат в терминал, вы увидите новое поле `offers`. В нём будут идентификаторы объектов объявлений. Всё лишнее мы обрезали. WIP: Задача не решена полностью
У нас есть список объявлений, которые относятся к определённой категории. В принципе, на этом можно и закончить. Необходимую информацию (количество) можно получить обратившись к свойству `length` массива `offers`. Но давайте пойдём дальше и попробуем решить эту задачу на этапе выборке. Для этого добавим новое поле с помощью оператора `$addFields`. Поле назовём `offerCount`. Его значением будет результат выполнения оператора `$size` для поля `$offers`. Оператор `$size` позволяет получить количество элементов в массиве. Обратите внимание, что мы дополнительно определяем поле `id`. В него мы сохраняем значение `_id`, предварительно сконвертировав его в строку. Это необходимо сделать, так как результат агрегации не будет соответствовать `CategoryEntity`. По структуре этот то же объект, но в нём не будет псевдонима `id`, который нам предоставляет `Typegoose`. Добавим его самостоятельно. В принципе, задача решена. Если посмотреть вывод списка категорий (сделать запрос), то видно, что поле `offerCount` появилось и заполняется. Но можно немного улучшить результат.
Немного улучшим агрегацию. Во-первых, толку от поля `offers` никакого. Мы им воспользовались один раз, чтобы посчитать количество элементов. Больше в нём нет необходимости. Раз так, можно от него избавиться, чтобы не передавать лишние данные. Для этого воспользуемся оператором `$unset`. Значением передаём название поля, которое нужно удалить. Затем ограничим вывод списка категорий шестью. Для этого мы завели константу `MAX_CATEGORIES_COUNT`. Ограничение сделаем с помощью оператора `$limit`. Последним шагом выполним сортировку выборки. Отсортируем категории по убыванию. Полем для сортировки станет `offerCount`. Сначала будут категории с бОльшим количеством объявлений. Всё, на этом закончим с агрегацией.
Для тестирования `nodemon` вернём `@injectable`.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
No description provided.