Учебники по программированию

Kotlin, Spring Data и MongoDB: разработка конечной точки API «GET /comments”».

С 4 вариантами поиска данных

TL;DR

Четыре вкуса:

  1. Получить ВСЕ – подробнее см. шаг 4.
  2. Фильтрация по точному соответствию – подробнее см. шаг 5.
  3. Фильтрация по частичному совпадению/совпадению подстроки – подробнее см. шаг 6
  4. Фильтрация с помощью полнотекстового поиска — подробнее см. шаг 7

Примеры кодов:https://github.com/geraldnguyen/kotlin-springdata-mongodb

#1 — Создайте проект Kotlin + Spring Data + MongoDB

Начнем с создания простого проекта с помощью Spring Initializr:

  • Язык: Котлин
  • Зависимости: Spring Data MongoDB, Spring Web

После того, как вы загрузите сгенерированный проект и настроите его на своем локальном компьютере, давайте запустим его, чтобы убедиться, что он работает. Вы можете заметить некоторые ошибки в консоли сервера, но это нормально, поскольку мы не настроили никаких деталей подключения к MongoDB.

#2 — Создайте бесплатный экземпляр MongoDB

Перейдите на https://www.mongodb.com/cloud/atlas/register, чтобы зарегистрировать бесплатный экземпляр MongoDB. Мы будем использовать общий кластер, потому что он подходит для наших целей обучения и является бесплатным.

Далее создаем пользователя БД

А затем настроить безопасность доступа к сети. Вы можете добавить 0.0.0.0/0 в поле IP-адреса, чтобы разрешить доступ из любого места.

Загрузите образцы данных

MongoDB Atlas поставляется с несколькими примерами баз данных. Давайте загрузим их в наш кластер.

Хотя для завершения загрузки всех баз данных требуется некоторое время, вы уже можете нажать кнопку «Обзор коллекции», чтобы изучить образцы данных.

# 3 — Настройка строки подключения MongoDB в Springboot

Найдите кнопку «Подключиться» на экране базы данных MongoDB.

Затем выберите Подключить приложение.

Затем выберите правильный драйвер (Java, последняя версия) и скопируйте строку подключения.

Вернитесь в свою IDE, отредактируйте конфигурацию приложения, чтобы добавить новую переменную среды MONGODB_URI со значением из скопированной строки подключения (с правильно обновленным паролем пользователя БД).

Давайте введем следующее в файл application.properties и перезапустим приложение. Ошибки подключения MongoDB должны исчезнуть.

spring.data.mongodb.uri=${MONGODB_URI}
spring.data.mongodb.database=sample_mflix

Обратите внимание, что мы указали образец базы данных «sample_mflix» в файле application.properties. Наше приложение будет подключаться к этой базе данных по умолчанию.

№ 4 — Вариант № 1: Получить все

Давайте создадим 3 новых класса Kotlin в нашем приложении:

  • <your-project>.api.CommentController: Контроллер REST для конечной точки /comments. Мы используем этот API для выборочного предоставления доступа к нашей базе данных посторонним. Это также наша основная среда для тестирования.
  • <your-project>.api.data.Comment: Модель данных для сбора комментариев
  • <your-project>.api.data.CommentRepository: репозиторий данных для сбора комментариев. Репозиторий — это интерфейс, в котором мы объявляем различные методы для получения данных или обновления данных в базе данных. Spring Data по умолчанию предоставляет готовые к использованию методы, такие как findAll или findById. Мы также можем объявить свои собственные. Фактическая реализация низкоуровневого протокола интеллектуально обеспечивается библиотекой данных Spring.

Модель данных комментариев

Класс Comment тесно связан с коллекциями «комментариев» в образце базы данных «sample_mflix».

При условии, что имя каждого поля совпадает со свойством коллекции, нам нужно только аннотировать класс @Document("<collection-name>") на уровне класса и @Id на уровне поля id, чтобы он работал.

@Field("movie_id") здесь необходим, потому что мы не хотим называть поле movie_id, так как это не соответствует соглашению об именах Kotlin.

@Document("comments")
class Comment {
    @Id
    lateinit var id: String

    lateinit var name: String

    lateinit var email: String

    @Field("movie_id")
    lateinit var movieId: String

    lateinit var text: String

    lateinit var date: LocalDateTime

}

Интерфейс репозитория комментариев

Если вы найдете класс Comment простым, вы будете рады увидеть содержимое файла CommentRepository.kt:

interface CommentRepository : MongoRepository<Comment, String>

Да, это однострочный.

Интерфейс CommentRepository является продолжением интерфейса Spring Data MongoRepository. Дополнительный <Comment, Spring> справа от MongoRepository предназначен для Spring Data, чтобы знать, для какой модели данных генерировать реализацию и какой первичный ключ использовать в этой модели.

Как я упоминал ранее, Spring Data предоставляет несколько готовых методов, таких как findAll или findById, которые мы можем использовать сразу.

Контроллер комментариев

Мы сохраним контроллер простым, но его все же достаточно, чтобы продемонстрировать реализацию первого варианта.

@RestController
@RequestMapping("/comments")
class CommentController {
    @Autowired
    private lateinit var commentRepository: CommentRepository

    @GetMapping(value = [ "", "/" ])
    fun listAll(): List<Comment> {
        return commentRepository.findAll()
    }
}

Главное, на что следует обратить внимание в этом контроллере, это commentRepository.findAll(). Этот оператор вызывает метод findAll() для commentRepository для извлечения всехComment записей из базы данных. Поскольку нам не нужна дополнительная обработка, мы просто return передаем результат вызывающей стороне.

Давайте перезапустим наше приложение и введем https://localhost:8080/comments в браузер.

Вы можете проверить ветку flavor_1, чтобы опробовать коды

Улучшить производительность CommentController

Если вы запустили приведенные выше коды варианта № 1, вы заметите много текста из большого документа JSON, отображаемого в браузере. Потребовалось некоторое время (10 секунд на моей машине), чтобы завершить загрузку.

Причина такой медлительности в том, что приложение действительно извлекло из базы данных все 41079 записей. Это просто слишком много рекордов! В результате пострадала производительность приложения. Бесплатный Mongo Atlas, вероятно, тоже не был бы доволен, если бы мы продолжали в том же духе.

Давайте улучшим его с помощью техники, называемой нумерацией страниц. См. код нового контроллера ниже.

@RestController
@RequestMapping("/comments")
class CommentController {
    @Autowired
    private lateinit var commentRepository: CommentRepository

    @GetMapping(value = [ "", "/" ])
    fun listAll(
        @RequestParam(value = "_size", required = false, defaultValue = "10") size: Int,
        @RequestParam(value = "_page", required = false, defaultValue = "0") page: Int
    ): List<Comment> {
        val pageable = Pageable.ofSize(size).withPage(page);

        val result: Page<Comment> = commentRepository.findAll(pageable)

        return result.content
    }
}

Вы могли заметить, что мы добавили 2 новых @RequestParam, а именно _size и _page. Они захватывают значения, указанные в параметре запроса URL с теми же именами. Они не являются обязательными и имеют значения по умолчанию, поэтому мы можем продолжать открывать https://localhost:8080/comments для загрузки первых 10 записей.

Мы используем эти параметры в операторе Pageable.ofSize(size).withPage(page) для создания объекта типа Pageable, который затем передаем в метод findAll репозитория. Spring Data будет использовать полученную информацию для разделения данных на несколько страниц (начиная с индекса 0) указанного _size, а затем извлекать данные из указанного _page. Возврат этого оператора findAll(Pageable) является объектом Page<Comment>, поэтому нам нужно получить доступresult.content, чтобы получить окончательный список, который мы просто возвращаем вызывающей стороне.

Давайте перезапустим приложение и попробуем открыть https://localhost:8080/comments.

Много улучшить! Вы так не думаете?

Теперь попробуем https://localhost:8080/comments?_size=100 или https://localhost:8080/comments?_size=2&_page=1, если заметите разницу.

Да, количество записей и позиции этих записей в коллекции базы данных изменяются в соответствии с параметрами _size и _page.

Вы можете проверить ветку flavor_1_with_pagination, чтобы изучить коды

Я использовал расширение JSON Formatter chrome, чтобы красиво отформатировать ответ. Ваш браузер может выглядеть иначе.

№ 5 — Вариант № 2: Фильтрация по точному совпадению

Как только мы можем получить список комментариев, иногда нам нравится подробно просматривать один комментарий. Поскольку каждый комментарий уникально идентифицируется своим атрибутом id, мы можем создать дополнительную конечную точку API /comments/<id> для получения точно интересующего нас комментария.

Давайте добавим следующую реализацию для этой конечной точки в файл CommentController. Что нового в этих кодах, так это использование метода findById(id) из экземпляра commentRepository для извлечения одной записи, точно соответствующей предоставленному значению id. Метод возвращает экземпляр Optional<Comment> (поскольку значение id может не совпадать ни с одной записью), поэтому нам нужно вызвать его метод get(), чтобы получить фактическую запись Comment.

    @GetMapping(value = ["/{id}"] )
    fun findById(@PathVariable("id") id: String): Comment {
        return commentRepository.findById(id).get();
    }

Перезапустим приложение и в адресной строке браузера введем https://localhost:8080/comments/5a9427648b0beebeb6957a4b

Вы можете проверить ветку git flavor_2_by_id, чтобы изучить коды.

На практике мы должны проверять предоставленное пользователем значение id и отклонять все вредоносные данные. Мы также должны проверить, действительно ли результат от findById() содержит какое-либо значение, прежде чем вызывать его метод get(). Однако для краткости такие коды в этом руководстве опущены.

Поиск комментариев по электронной почте

Давайте немного улучшим наше приложение, включив поиск по электронной почте.

Чтобы включить эту функцию, нам сначала нужно включить эту возможность в CommentRepository, добавив метод findByEmailIgnoreCase(email: String, pageable: Pageable). Обратите внимание, как имя метода состоит из findBy + Email + IgnoreCase и что нам не нужно предоставлять какую-либо реализацию. Это магия Spring Data. Из имени он знает, что мы хотим найти все записи, соответствующие атрибуту email (который передается в качестве первого параметра) и игнорировать регистр. (например, [email protected] совпадает с [email protected]). Нам по-прежнему нравится разбивать результат на страницы, поэтому мы сохраняем параметр Pageable.

interface CommentRepository : MongoRepository<Comment, String>{
    fun findByEmailIgnoreCase(email: String, pageable: Pageable): Page<Comment>
}

На CommentController нам нужно сделать небольшую модификацию, чтобы вызвать новый метод CommentRepository. Обратите внимание на новый параметр email. Если он содержит какое-то значение (т.е. это не isNullOrBlank()), то мы будем вызывать commentRepository.findByEmailIgnoreCase(email, pageable) для поиска всех комментариев, соответствующих указанному значению email, независимо от его регистра.

@GetMapping(value = [ "", "/" ])
fun listAll(
    @RequestParam(value = "email", required = false) email: String?,
    @RequestParam(value = "_size", required = false, defaultValue = "10") size: Int,
    @RequestParam(value = "_page", required = false, defaultValue = "0") page: Int
): List<Comment> {
    lateinit var result: Page<Comment>
    val pageable = Pageable.ofSize(size).withPage(page);

    when {
        !email.isNullOrBlank() ->
            result = commentRepository.findByEmailIgnoreCase(email, pageable)
        else ->
            result = commentRepository.findAll(pageable)
    }

    return result.content
}

Давайте проверим это, перезапустив приложение и посетив https://localhost:8080/[email protected] или любое другое электронное письмо, которое вас заинтересует.

Имея всего 1 дополнительный параметр из предыдущей версии, мы могли бы использовать if...else вместо when. Однако, как вы увидите в следующем разделе, использование when позволяет нам поддерживать другие разновидности с более чистым кодом.

Вы можете проверить ветку git flavor_2_by_id, чтобы изучить коды.

№ 6 — Вариант № 3: Фильтрация по частичному совпадению/совпадению подстроки

Благодаря функции поиска по электронной почте наши пользователи были очень довольны. Некоторое время. Потом потребовали еще. На этот раз они хотели иметь возможность поиска комментариев по имени автора. Вы думаете, что это легко — это аромат № 2, вы уже знали, как это сделать. Нет, наши пользователи не хотят вводить полное имя, а только его часть, например, имя или фамилию.

Это аромат №3. Мы сделаем это в этом разделе.

Секретный ингредиент аромата №3 находится в CommentRepository. Как и в случае с поиском по электронной почте, нам сначала нужно создать подходящий метод. Новый метод findByNameContainingIgnoreCase(name: String, pageable: Pageable) очень похож на своего старшего брата, за исключением ключевого слова Containing, следующего за именем соответствующего атрибута. Containing — это ключевое слово Spring Data, которое указывает Spring выполнять частичное сопоставление или сопоставление подстрок вместо точного сопоставления. Containing вернет запись с именем «John Doe» при поиске по «John» или «Doe».

interface CommentRepository : MongoRepository<Comment, String>{
    fun findByEmailIgnoreCase(email: String, pageable: Pageable): Page<Comment>
    fun findByNameContainingIgnoreCase(name: String, pageable: Pageable): Page<Comment>
}

Оставшаяся задача в CommentController проста. Обратите внимание, как мы добавляем новый параметр name и новое условие соответствия в операторе when.

@GetMapping(value = [ "", "/" ])
fun listAll(
    @RequestParam(value = "email", required = false) email: String?,
    @RequestParam(value = "name", required = false) name: String?,
    @RequestParam(value = "_size", required = false, defaultValue = "10") size: Int,
    @RequestParam(value = "_page", required = false, defaultValue = "0") page: Int
): List<Comment> {
    lateinit var result: Page<Comment>
    val pageable = Pageable.ofSize(size).withPage(page);

    when {
        !email.isNullOrBlank() ->
            result = commentRepository.findByEmailIgnoreCase(email, pageable)
        !name.isNullOrBlank() ->
            result = commentRepository.findByNameContainingIgnoreCase(name, pageable)
        else ->
            result = commentRepository.findAll(pageable)
    }

    return result.content
}

Опять же, давайте проверим это, перезапустив приложение, а затем открыв https://localhost:8080/comments?name=roman в браузере.

Вы можете проверить ветку git flavor_3, чтобы изучить коды.

№7 — Вариант №4 — Полнотекстовый поиск

Наши пользователи хотят большего. Им нужен общий поиск по имени, адресу электронной почты и содержанию самого комментария, и они хотят, чтобы поиск был более интеллектуальным. Например, они хотели «ранил». и "выполнить", чтобы вернуть те же результаты, так что "идти", "идти" и "пошли ", а также "платье" и "платья" и т. д. Эти возможности выходят за рамки того, что могут предложить вкусы № 1, № 2 и № 3.

На помощь приходит вариант №4 — Полнотекстовый поиск. Эта функция просто требует дополнительной настройки на стороне БД, но в остальном она проста.

Включение индексации в базе данных MongoDB

Давайте откроем коллекцию «comments» в базе данных «sample_mflix» в вашем кластере MongoDB. Затем откройте вкладку «Индексы» и нажмите кнопку «Создать индекс». Это должно вызвать модальное окно, подобное приведенному ниже.

Поместите то же содержимое, что и ниже, в текстовую область «Поля», нажмите «Обзор», а затем «Подтвердить», чтобы создать индекс.

{
  "name": "text",
  "email": "text",
  "text": "text"
}

Что мы только что сделали, так это создали текстовый индекс для 3 полей имя, электронная почта и текст коллекции комментарии. Текстовый индекс обеспечивает полнотекстовый поиск по указанным полям.

Добавление параметра поиска «ключевое слово»

Вернемся к нашим кодам Kotlin и Spring Data. Мы добавим новый параметр поиска с именем keyword с небольшой модификацией параметра CommentController.

@GetMapping(value = [ "", "/" ])
fun listAll(
    @RequestParam(value = "email", required = false) email: String?,
    @RequestParam(value = "name", required = false) name: String?,
    @RequestParam(value = "keyword", required = false) keyword: String?,
    @RequestParam(value = "_size", required = false, defaultValue = "10") size: Int,
    @RequestParam(value = "_page", required = false, defaultValue = "0") page: Int
): List<Comment> {
    lateinit var result: Page<Comment>
    val pageable = Pageable.ofSize(size).withPage(page);

    when {
        !email.isNullOrBlank() ->
            result = commentRepository.findByEmailIgnoreCase(email, pageable)
        !name.isNullOrBlank() ->
            result = commentRepository.findByNameContainingIgnoreCase(name, pageable)
        !keyword.isNullOrBlank() ->
            result = commentRepository.findAllBy(TextCriteria.forDefaultLanguage().matching(keyword), pageable)
        else ->
            result = commentRepository.findAll(pageable)
    }

    return result.content
}

Обратите внимание на это утверждение commentRepository.findAllBy(TextCriteria.forDefaultLanguage().matching(keyword), pageable). Что он делает, так это создает критерий полнотекстового поиска, соответствующий указанному keyword, затем мы отправляем этот критерий в CommentRepository для пересылки на сервер MongoDB.

Конечно, нам нужно добавить в репозиторий метод findAllBy(TextCriteria, Pageable)

interface CommentRepository : MongoRepository<Comment, String>{
    fun findByEmailIgnoreCase(email: String, pageable: Pageable): Page<Comment>
    fun findByNameContainingIgnoreCase(name: String, pageable: Pageable): Page<Comment>
    fun findAllBy(textCriteria: TextCriteria, pageable: Pageable): Page<Comment>
}

Это все, что нужно для выполнения полнотекстового поиска.

Давайте перезапустим приложение и попробуем разные ключевые слова, такие как:

  • «gameofthron» для электронной почты
  • «roberts» для name — обратите внимание, что результаты поиска содержат совпадения как для «Robert», так и для «Roberts».
  • и «officiis» для текста

Вы можете проверить ветку flavor_4, чтобы изучить коды

Заключение

Мы прошли довольно много шагов. Главное, мы научились:

  • Настройте приложение Kotlin + Spring Data + MongoDB
  • Создайте бесплатный кластер MongoDB и загрузите несколько примеров баз данных.
  • Создайте текстовый индекс в коллекции MongoDB, чтобы включить полнотекстовый поиск.
  • Создайте методы репозитория REST API и MongoDB для поиска в MongoDB.
  • Реализуйте 4 разновидности поиска данных: от получения всех до точного сопоставления, частичного поиска и, наконец, полнотекстового поиска.

Если вам понравился этот урок, пожалуйста, подпишитесь, чтобы получать мою последнюю статью на вашу электронную почту. Спасибо.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу