Проект, созданный в рамках изучения Spring Data JPA, Docker, Hibernate, Mock и Spring Boot тестирования
Идея заключается в создании приложения для регистрации пользователей и возможности создания заявок на временное пользование вещами, которые могут добавлять пользователи. В проекте также предусмотрена возможность оставлять заявки на использование тех или иных вещей. Я использую две базы данных: PostgreSQL (основная) и H2 (для тестирования работоспособности приложения).
Spring, PostgreSQL, Hibernate, H2, SLF4J, Lombok, Spring Data JPA, Docker и Mock.
Часть 1
В этом модуле я создаю сервис для шеринга (от англ. share — «делиться») вещей. Шеринг как экономика совместного использования набирает всё большую популярность. Если в 2014 году глобальный рынок шеринга оценивался всего в $15 млрд, то к 2025 может достигнуть $335 млрд.
Почему шеринг так популярен? Представьте, что на воскресной ярмарке я купил несколько картин и хочу повесить их дома. Но вот незадача — для этого нужна дрель, а её у меня нет. Я могу пойти в магазин и купить, но в такой покупке мало смысла — после того, как я повешу картины, дрель будет просто пылиться в шкафу. Я могу пригласить мастера — но за его услуги придётся заплатить. И тут я вспоминаю, что видел дрель у друга. Сама собой напрашивается идея — одолжить её.
Большая удача, что у меня оказался друг с дрелью, и я сразу вспомнил про него! А не то в поисках инструмента пришлось бы писать всем друзьям и знакомым. Или вернуться к первым двум вариантам — покупке дрели или найму мастера. Насколько было бы удобнее, если бы под рукой был сервис, где пользователи делятся вещами! Созданием такого проекта я и занимаюсь.
Мой проект называется ShareIt
. Он должен обеспечить пользователям возможность рассказывать, какими вещами они готовы поделиться, а также находить нужную вещь и брать её в аренду на какое-то время.
Сервис не только позволяет бронировать вещь на определённые даты, но и закрывать к ней доступ на время бронирования от других желающих. Если нужной вещи на сервисе нет, пользователи могут возможность оставлять запросы. Вдруг древний граммофон, который странно даже предлагать к аренде, неожиданно понадобится для театральной постановки. По запросу можно будет добавлять новые вещи для шеринга.
В этом спринте от меня требуется создать каркас приложения, а также разработать часть его веб-слоя. Основная сущность сервиса, вокруг которой будет строиться вся дальнейшая работа, — вещь. В коде она будет фигурировать как Item
. Пользователь, который добавляет в приложение новую вещь, будет считаться её владельцем. При добавлении вещи есть возможность указать её краткое название и добавить небольшое описание. Например, название может быть — «Дрель “Салют”», а описание — «Мощность 600 вт, работает ударный режим, так что бетон возьмёт». Также у вещи обязательно должен быть статус — доступна ли она для аренды. Статус должен проставлять владелец.
Для поиска вещей должен быть организован поиск. Чтобы воспользоваться нужной вещью, её требуется забронировать. Бронирование, или Booking — ещё одна важная сущность приложения. Бронируется вещь всегда на определённые даты. Владелец вещи обязательно должен подтвердить бронирование.
После того как вещь возвращена, у пользователя, который её арендовал, должна быть возможность оставить отзыв. В отзыве можно поблагодарить владельца вещи и подтвердить, что задача выполнена — дрель успешно справилась с бетоном, и картины повешены.
Ещё одна сущность, которая мне понадобится, — запрос вещи ItemRequest. Пользователь создаёт запрос, если нужная ему вещь не найдена при поиске. В запросе указывается, что именно он ищет. В ответ на запрос другие пользователи могут добавить нужную вещь.
Также готов шаблон проекта с использованием Spring Boot
. Я создала ветку add-controllers
и переключился на неё — в этой ветке будет вестись вся разработка для 1го спринта.
В этом модуле я использую структуру не по типам классов, а по фичам (англ. Feature layout) — весь код для работы с определённой сущностью в одном пакете. Поэтому я сразу создала четыре пакета — item
, booking
, request
и user
. В каждом из этих пакетов будут свои контроллеры, сервисы, репозитории и другие классы, которые мне понадобятся в ходе разработки. В пакете item
я создала класс Item
.
Созданные объекты Item и User я буду использовать для работы с базой данных (это ждёт меня в следующем спринте). Сейчас, помимо них, мне также понадобятся объекты, которые я буду возвращать пользователям через REST-интерфейс в ответ на их запросы.
Разделять объекты, которые хранятся в базе данных и которые возвращаются пользователям, — хорошая практика. Например, я могу не захотеть показывать пользователям владельца вещи (поле owner), а вместо этого возвращать только информацию о том, сколько раз вещь была в аренде. Чтобы это реализовать, нужно создать отдельную версию каждого класса, с которой будут работать пользователи — DTO (Data Transfer Object).
Кроме DTO-классов, понадобятся Mapper-классы — они помогут преобразовывать объекты модели в DTO-объекты и обратно. Для базовых сущностей Item и User я создам Mapper-класс и метод преобразования объекта модели в DTO-объект.
Когда классы для хранения данных будут готовы, я могу перейти к реализации логики. В моем приложении будет три классических слоя — контроллеры, сервисы и репозитории. В этом спринте я буду работать преимущественно с контроллерами.
Для начала мне нужно научить приложение работать с пользователями. Ранее я уже создавал контроллеры для управления пользователями — создания, редактирования и просмотра. Теперь я сделаю то же самое: создам класс UserController и реализую методы для основных CRUD-операций. Также я реализую сохранение данных о пользователях в памяти.
Далее я перейду к основной функциональности этого спринта — работе с вещами. Мне нужно реализовать добавление новых вещей, их редактирование, просмотр списка вещей и поиск. Я создам класс ItemController, в котором будет сосредоточен весь REST-интерфейс для работы с вещами.
Вот основные сценарии, которые я должен поддерживать в приложении:
*Добавление новой вещи. Это будет происходить по эндпойнту POST /items. На вход будет поступать объект ItemDto. userId в заголовке X-Sharer-User-Id — это идентификатор пользователя, который добавляет вещь. Именно этот пользователь будет владельцем вещи. Идентификатор владельца будет поступать на вход в каждом из запросов.
*Редактирование вещи. Эндпойнт PATCH /items/{itemId}. Я смогу изменить название, описание и статус доступа к аренде, и редактировать вещь сможет только её владелец.
*Просмотр информации о конкретной вещи по её идентификатору. Эндпойнт GET /items/{itemId}. Информацию о вещи сможет просмотреть любой пользователь.
*Просмотр владельцем списка всех его вещей с указанием названия и описания для каждой. Эндпойнт GET /items.
Поиск вещи потенциальным арендатором. Пользователь будет передавать в строке запроса текст, и система будет искать вещи, содержащие этот текст в названии или описании. Это будет происходить по эндпойнту /items/search?text={text}. В text я передам текст для поиска и проверю, что поиск возвращает только доступные для аренды вещи.
Для каждого из этих сценариев я создам соответствующий метод в контроллере. Также я создам интерфейс ItemService и реализующий его класс ItemServiceImpl, к которому будет обращаться мой контроллер. В качестве DAO я создам реализации, которые будут хранить данные в памяти приложения. Работу с базой данных я реализую в следующем спринте.
Для проверки кода я подготовил Postman-коллекцию.. С её помощью я смогу протестировать API и убедиться, что все запросы успешно выполняются.
Если задание покажется недостаточно подробным, я могу обратиться к дополнительным материалам, которые помогут мне выполнить задание спринта. Но я помню, что реальные технические задания часто скупы на подробности, поэтому разработчику приходится самостоятельно принимать некоторые архитектурные решения. Чем раньше я научусь определять минимальные требования, необходимые для начала разработки проекта, тем проще мне будет работать в команде над реальным проектом: Дополнительные советы ментора.
Часть 2
В прошлом спринте вы приступили к проекту ShareIt
и уже сделали немало — например,
реализовали слой контроллеров для работы с вещами. В этот раз вы продолжите совершенствовать сервис, так что он станет
по-настоящему полезным для пользователей.
Перед вами две большие задачи: добавить работу с базой данных в уже реализованную часть проекта, а также дать пользователям возможность бронировать вещи.
В этом спринте разработка будет вестись в ветке add-bookings
. Создайте ветку с таким названием и переключитесь на неё.
Далее переходите к настройке базы данных. Пришло время использовать Hibernate и JPA самостоятельно. Для начала добавьте
зависимость spring-boot-starter-data-jpa
и драйвер postgresql
в файл pom.xml
.
Теперь поработайте над структурой базы данных. В ней будет по одной таблице для каждой из основных сущностей, а также таблица, где будут храниться отзывы.
Подумайте, какой тип данных PostgreSQL лучше подойдёт для каждого поля. В качестве подсказки проанализируйте таблицы,
которые были использованы в приложении Later
.
Напишите SQL-код для создания всех таблиц и сохраните его в файле resources/schema.sql
— Spring Boot выполнит
содержащийся в нём скрипт на старте проекта. На данный момент вам достаточно создать таблицы для двух сущностей,
которые вы уже разработали — Item
и User
.
Важный момент: приложение будет запускаться много раз, и каждый раз Spring будет выполнять schema.sql
. Чтобы ничего не
сломать и не вызвать ошибок, все конструкции в этом файле должны поддерживать множественное выполнение. Это значит, что
для создания таблиц следует использовать не просто конструкцию CREATE TABLE
, но CREATE TABLE IF NOT EXIST
— тогда
таблица будет создана, только если её ещё не существует в базе данных.
CREATE TABLE IF NOT EXISTS users (
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(512) NOT NULL,
CONSTRAINT pk_user PRIMARY KEY (id),
CONSTRAINT UQ_USER_EMAIL UNIQUE (email)
);
Подготовка сущности к работе с базой данных. Для этого использую аннотации JPA:
@Entity
, @Table
, @Column
, @Id
. Для поля status
в классе Booking
- @Enumerated
. Добавьте
соответствующие аннотации для сущностей.
Создайте репозитории для User
и Item
и доработайте сервисы, чтобы они работали с новыми репозиториями.
Если название поля в модели отличается от имени поля в базе, ууказаваю маппинг между ними с
помощью аннотации @Column
.
Чтобы сделать приложение ещё более полезным и интересным, добалена возможность брать вещи в аренду на определённые даты.
Вот основные сценарии и эндпоинты:
- Добавление нового запроса на бронирование. Запрос может быть создан любым пользователем, а затем подтверждён владельцем вещи.
Эндпоинт —
POST /bookings
. После создания запрос находится в статусеWAITING
— «ожидает подтверждения». - Подтверждение или отклонение запроса на бронирование. Может быть выполнено только владельцем вещи. Затем статус
бронирования становится либо
APPROVED
, либоREJECTED
. Эндпоинт —PATCH /bookings/{bookingId}?approved={approved}
, параметрapproved
может принимать значенияtrue
илиfalse
. - Получение данных о конкретном бронировании (включая его статус). Может быть выполнено либо автором бронирования, либо
владельцем вещи, к которой относится бронирование. Эндпоинт —
GET /bookings/{bookingId}
. - Получение списка всех бронирований текущего пользователя. Эндпоинт —
GET /bookings?state={state}
. Параметр state необязательный и по умолчанию равенALL
(англ. «все»). Также он может принимать значенияCURRENT
(англ. «текущие»),**PAST**
(англ. «завершённые»),FUTURE
(англ. «будущие»),WAITING
(англ. «ожидающие подтверждения»),REJECTED
(англ. «отклонённые»). Бронирования должны возвращаться отсортированными по дате от более новых к более старым. - Получение списка бронирований для всех вещей текущего пользователя. Эндпоинт —
GET /bookings/owner?state={state}
. Этот запрос имеет смысл для владельца хотя бы одной вещи. Работа параметраstate
аналогична его работе в предыдущем сценарии.
Для начала добавлена в модель данных сущность Booking
и код для создания соответствующей таблицы в файл resources/schema.sql
.
Создан контроллер BookingController
и методы для каждого из описанных сценариев.
Кроме контроллеров, реализовано хранение данных — то есть сервисы и репозитории.
Например, полезно создать отдельное перечисление для возможных методов параметра state
, ведь задачи этого
перечисления могут отличаться в слое представления (параметр для поиска) и в модели данных (состояние бронирования).
Добавила, чтобы владелец видел даты
последнего и ближайшего следующего бронирования для каждой вещи, когда просматривает список (GET /items
).
Пользователи смогут оставлять отзывы на вещь после того, как взяли её в аренду.
В базе данных уже есть таблица comments
. Создан соответствующий класс модели данных Comment
и добавлены
необходимые аннотации JPA. Поскольку отзыв — вспомогательная сущность и по сути часть вещи, отдельный пакет для отзывов
не нужен. Помещен класс в пакет item
.
Комментарий можно добавить по эндпоинту POST /items/{itemId}/comment
, в контроллере создан метод для него.
Реализовала логику по добавлению нового комментария к вещи в сервисе ItemServiceImpl
. Для этого создала
интерфейс CommentRepository
. Добавила проверку, что пользователь, который пишет комментарий, действительно
брал вещь в аренду.
Разрешила пользователям просматривать комментарии других пользователей. Отзывы можно будет увидеть по двум
эндпоинтам — по GET /items/{itemId}
для одной конкретной вещи и по GET /items
для всех вещей данного пользователя.
Для проверки всей функциональности в этом спринте использовала Postman-коллекцию — для тестирования приложения.
Часть 3
*Добавила возможность создавать запрос вещи и добавлять вещи в ответ на запросы других пользователей. *Реализовать тесты для всего приложения.
В этом спринте разработка велась в ветке add-item-requests
. Функциональность:
запроса на добавление вещи. Её суть в следующем.
Пользователь можен создать такой запрос, когда не может найти нужную вещь, воспользовавшись поиском, но при этом надеется, что у кого-то она всё же имеется. Другие пользователи могут просматривать подобные запросы и, если у них есть описанная вещь и они готовы предоставить её в аренду, добавлять нужную вещь в ответ на запрос.
Поэтому добавлено четыре новых эндпоинта:
POST /requests
— добавить новый запрос вещи. Основная часть запроса — текст запроса, где пользователь описывает, какая именно вещь ему нужна.GET /requests
— получить список своих запросов вместе с данными об ответах на них. Для каждого запроса должны указываться описание, дата и время создания и список ответов в формате:id
вещи, название, её описаниеdescription
, а такжеrequestId
запроса и признак доступности вещиavailable
. Так в дальнейшем, используя указанныеid
вещей, можно будет получить подробную информацию о каждой вещи. Запросы должны возвращаться в отсортированном порядке от более новых к более старым.GET /requests/all?from={from}&size={size}
— получить список запросов, созданных другими пользователями. С помощью этого эндпоинта пользователи смогут просматривать существующие запросы, на которые они могли бы ответить. Запросы сортируются по дате создания: от более новых к более старым. Результаты должны возвращаться постранично. Для этого нужно передать два параметра:from
— индекс первого элемента, начиная с 0, иsize
— количество элементов для отображения.GET /requests/{requestId}
— получить данные об одном конкретном запросе вместе с данными об ответах на него в том же формате, что и в эндпоинтеGET /requests
. Посмотреть данные об отдельном запросе может любой пользователь.
Добавлена ещё одна полезная опция в приложение, чтобы пользователи могли отвечать на запросы друг друга. Для этого при
создании вещи есть возможность указать id
запроса, в ответ на который создаётся нужная вещь.
Так же добавлено поле requestId
в тело запроса POST /items
. Обратите внимание, там сохраниться возможность добавить
вещь и без указания requestId
.
Использована в запросе GET /requests/all
пагинация,
поскольку запросов может быть очень много.
Если пользователи жалуются, что запросы возвращают слишком много данных и с ними невозможно работать. Эта проблема
возникает при просмотре бронирований и особенно при просмотре вещей. Поэтому, чтобы приложение было комфортным
для пользователей, а также быстро работало, добавлена пагинация в эндпоинты GET /items
,
GET /items/search
, GET /bookings
и GET /bookings/owner
.
Параметры такие же, как и для эндпоинта на получение запросов вещей: номер первой записи и желаемое количество элементов для отображения.
Сформулировала функциональные и нефункциональные требования к приложению. В соответствии с этими требованиями написала реализацию, после этого написала юнит-тесты, проверяющие реализацию на соответствие требованиям.
- Реализовала юнит-тесты для всего кода, содержащего логику. Выбрала классы, которые содержат в себе нетривиальные методы, условия и ветвления. В основном это классы сервисов. Написала юнит-тесты на все такие методы, используя моки при необходимости.
- Реализовала интеграционные тесты, проверяющие взаимодействие с базой данных. Интеграционные тесты
представляют собой более высокий уровень тестирования: их обычно требуется меньше, но покрытие каждого — больше.
Поэтому создала по одному интеграционному тесту для каждого крупного метода в ваших сервисах.
Для метода
getUserItems
в классеItemServiceImpl
. - Реализовала тесты для REST-эндпоинтов вашего приложения с использованием
MockMVC
. Покрыла тестами все существующие эндпоинты. При этом для слоя сервисов использовала моки. - Реализовала тесты для слоя репозиториев вашего приложения с использованием аннотации
@DataJpaTest
. Написала тесты для тех репозиториев, которые содержат кастомные запросы. Работа с аннотацией@DataJpaTest
подсказка. Ещё больше деталей по совету. - Реализовала тесты для работы с JSON для DTO в вашем приложении с помощью аннотации
@JsonTest
. Такие тесты имеют смысл в тех случаях, когда ваши DTO содержат в себе некоторую логику. Например, описание формата дат или валидацию. Поэтому я выбрала DTO, где есть подобные условия, и написала тесты.
Для проверки реализованной функциональности postman-тесты.
Часть 4
Например, пользователей приложения ShareIt
становится больше. Становится заметно, что не всё идёт гладко: приложение
работает медленнее, пользователи чаще жалуются, что их запросы подолгу остаются без ответа.
Некоторые пользователи используют ваше приложение через другие программы: собственноручно
написанные интерфейсы, боты… Чего они только не придумали!
Не все эти программы работают правильно. В ShareIt
поступает много некорректных запросов — например, с невалидными
входными данными, в неверном формате или просто дублей. Приложение тратит ресурсы на обработку каждого из запросов,
и в результате его работа замедляется. Нужно разобраться с этим!
В реальной разработке для решения подобных проблем часто применяется микросервисная архитектура. Можно вынести часть приложения, с которой непосредственно работают пользователи, в отдельное небольшое
приложение и назвать его, допустим, **gateway**
(англ. «шлюз»). В нём будет выполняться вся валидация запросов —
некорректные будут исключаться.
Поскольку для этой части работы не требуется базы данных и каких-то особых ресурсов, приложение gateway
будет
легковесным. При необходимости его получится легко масштабировать. Например, вместо одного экземпляра gateway
можно
запустить целых три — чтобы справиться с потоком запросов от пользователей.
После валидации в gateway
запрос будет отправлен основному приложению, которое делает всю реальную работу — в том числе
обращается к базе данных. Также на стороне gateway может быть реализовано кэширование: например, если один и тот же
запрос придёт два раза подряд, gateway
будет самостоятельно возвращать предыдущий ответ без обращения к основному
приложению.
Вся работа в этом спринте ведется в ветке add-docker
.
- Разбила приложение
ShareIt
на два —shareIt-server
иshareIt-gateway
. Они будут общаться друг с другом через REST. Вынесла вshareIt-gateway
всю логику валидации входных данных — кроме той, которая требует работы с БД. - Настроила запуск
ShareIt
через Docker. ПриложенияshareIt-server
,shareIt-gateway
и база данных PostgreSQL должны - запускаться в отдельном Docker-контейнере каждый. Их взаимодействие настроено через Docker Compose.
Приложение shareIt-server
содержит всю основную логику и почти полностью повторяет приложение, за исключением того, что убрана валидация данных в контроллерах.
Во второе приложение shareIt-gateway
вынесены контроллеры, с которыми непосредственно работают пользователи, —
вместе с валидацией входных данных.
Каждое из приложений запускается как самостоятельное Java-приложение, а их общение происходит через REST. Чтобы сделать запуск и взаимодействие приложений более предсказуемым и удобным, каждое из них размещено в своём Docker-контейнере. Также вынесены в Docker-контейнер базу данных.
Приложение ShareIt
разбино на два так, что оба остались в том же репозитории и собирались одной
Maven-командой. Реализованный подобный механизм в Maven помогают многомодульные проекты (англ. multi-module project).
Такие проекты содержат в себе несколько более мелких подпроектов.
В нашем случае каждый из подпроектов будет представлять собой самостоятельное Java-приложение. Вообще же подпроект может содержать любой набор кода или других сущностей, которые собираются с помощью Maven. Это может быть, например, набор статических ресурсов — HTML-файлы, изображения и так далее.
Многомодульный проект содержит один родительский pom
-файл для всего проекта, в котором перечисляются все модули или
подпроекты. Также для каждого из модулей создан собственный pom
-файл со всей информацией о сборке отдельного
модуля. Когда в корневой директории проекта запускается команда сборки (например, mvn clean install
), Maven соберёт
каждый из модулей и положит результирующий jar
-файл в директорию target
соответствующего модуля.
💡 узнала, как работать с многомодульными проектами из этого ресурса. Шаблон многомодульного проекта : add-docker.
Распределила код приложения между модулями, а также добавила в shareIt-gateway
код для обращения к
shareIt-server
через REST.
Чтобы вам было проще работать с REST, создала в shareIt-gateway
класс BaseClient
, который реализует базовый
механизм взаимодействия через REST.
использованная информация для работы с REST-вызовами, а также в
«Дополнительных советах ментора».
Подготовила Dockerfile
для каждого из сервисов — shareIt-server
и shareIt-gateway
. Шаблон для этих файлов расположен
в корневой папке каждого модуля, его содержимое такое же, как и в теме про Docker. Описала настройки
развёртывания контейнеров в файле docker-compose.yaml
в корне проекта. Конфигурация развёртывания включает три
контейнера для следующих сервисов: shareIt-server
, shareIt-gateway
и postgresql
.
💡 Для целей разработки можно запускать каждый из сервисов локально через IDE, а работу через Docker проверять после завершения очередного этапа разработки. Перед тем как тестировать новую версию в Docker, обязательно пересобирается код проекта и удаляется старый Docker-образ!
Запустить приложение можно командой docker-compose up
и пользователи, как и прежде,
могут создавать и бронировать вещи.
Postman-коллекцией, чтобы протестировать работу приложения.