11 мая 2018
Тэги: Java, PostgreSQL, Spring Boot.
В предыдущих статьях мы уже создавали rest-приложение (Spring Boot Restful Service, Работа с БД в Spring Boot на примере postgresql). А теперь давайте рассмотрим, как работать с датой и временем в Spring Boot на уровне rest-запросов и на уровне БД.
Предположим, перед нами стоит задача фиксировать в специальной таблице все действия пользователя (регистрация, вход, выход и т.п.) Таблица для СУБД Postgres в самом простом случае будет выглядеть так:
Тип serial представляет собой поле, которое автоматически увеличивается на единицу для каждой новой записи, поэтому его удобно использовать в качестве первичного ключа для записи.
Тип timestamp without time zone позволяет хранить метку времени без привязки к часовому поясу.
user_id и action_type представляют собой числовые id пользователя и тип действия соответственно. В реальном приложении каждое из них должно быть внешним ключом на соответствующие таблицы, но в нашем примере для простоты такой привязки нет.
Для типов действий удобно создать enum, чтобы не запоминать конкретные id.
В этом enum имеется приватное поле id. Возможные значения (LOGIN, LOGOUT) инициализируются тут же через приватный конструктор. Статический метод getById() позволяет по числовому значению получить одно из значений enum, что очень удобно при чтении записи из БД. Все доступные значения мы получаем через метод values() и последовательно проверяем их. Как только нашли соответствие – сразу же возвращаем его, выходя из метода (а значит, и из цикла). Если же вдруг мы не смогли найти подходящего значения для указанного id – это является ошибкой времени выполнения и мы кидаем исключение после цикла. Однако если вы всегда пишете в БД тип действия при помощи этого enum, то такой ошибки произойти не может.
Теперь мы готовы реализовать добавление записи о действии пользователя на уровне БД.
Здесь в общем-то всё прозрачно. Формируем параметры запроса через класс MapSqlParameterSource, а затем выполняем sql-запрос через метод update() класса NamedParameterJdbcTemplate. В тексте запроса значения параметров начинаются с двоеточия. В параметры дата должна передаваться в виде Timestamp. Но у этого класса есть статический метод valueOf(), который на вход принимает именно наш LocalDateTime.
Класс, в который будет мапиться json-запрос на добавление записи выглядит так:
Здесь мы используем валидацию параметров. Аннотация @NotNull означает, что параметр обязательный. Чтобы корректно работала эта проверка с числовыми типами, в запросе используем не примитивный int, который по умолчанию примет значение 0, а объектный Integer, который по умолчанию null. Аннотация @Min указывает, какое минимальное значение допустимо для числового поля. В данном примере исходим из того, что все id положительные и начинаются с 1.
Пример json-запроса, который соответствует этому классу:
Обратите внимание, что параметр «action» имеет здесь не числовой тип, а строковый, причём допустимые значения – это имена значений из перечисления ActionType.
Не менее интересен формат даты и времени. Сначала идёт год, затем месяц, затем день, затем литерал «T», затем время. Это стандартный формат даты, но чтобы он корректно работал в Spring Boot, нужно добавить новую dependency в наш pom-файл:
Также нужно добавить новый параметр в конфигурационный файл приложения, путь до которого указывается в командной строке через параметр --spring.config.location:
Теперь мы готовы написать наш rest-контроллер:
В rest-сервисах добавление новых записей происходит черeз POST-запрос (аннотация @PostMapping). Через аннотацию @ResponseStatus мы указываем, что http-статус ответа будет 201 – «Created». @RequestBody указывает, какой именно параметр метода должен быть связан с телом json-запроса, а @Valid нужна для того, чтобы работала валидация параметров, которую мы рассматривали выше.
Теперь мы можем запустить наше приложение и отправить указанный выше POST-запрос. Не забудьте добавить http-заголовок «Content-Type: application/json». Из командной строки запрос можно выполнить так:
Если всё сделано правильно, в нашей таблице появится новая запись.
А теперь давайте создадим запрос на чтение ранее добавленных записей, причём будем фильтровать по дате добавления, передавая на вход нужный нам диапазон дат. На выходе будем получать список из тех же объектов UserAction, который использовали ранее. Поэтому нам понадобится RowMapper, который будет построчно преобразовывать полученный из БД набор данных в нужные нам объекты:
При чтении данных из БД для даты действия получаем объект Timestamp, который имеет встроенный метод для преобразования в привычный нам LocalDateTime.
Теперь добавим в UserActionDao метод для чтения истории. Также нам потребуется подгрузить наш маппер:
Обратите внимание, что метод получает на вход LocalDate, т.е. дату без времени. Указывая диапазон дат, мы подразумеваем промежуток с первой секунды начальной даты до последней секунды конечной даты. Поэтому преобразуем дату в дату со временем при помощи метода atTime() и констант, доступных в классе LocalTime.
Теперь добавим новый метод в наш контроллер:
Аннотация @GetMapping определяет, какой тип запроса и какой адрес связывается с данным методом, причём здесь мы указываем переменную userId. Её значение подставляем в соответствующий параметр при помощи аннотации @PathVariable. Поскольку у GET-запроса не может быть body, в отличие от POST и PUT запросов, диапазон передаём в виде параметров запроса (аннотация @RequestParam). Формат даты задаём через @DateTimeFormat («ГГГГ-ММ-ДД»). В теле обработчика вызываем наш dao-слой.
Если всё сделано правильно, нам достаточно запустить наше приложение и выполнить следующий GET-запрос:
В этом примере мы запрашиваем историю действий для пользователя с userId=1 с 10 по 11 мая 2018 года. Формат ответа будет таким:
В итоге получаем список ровно из таких же элементов, которые передавали в запросе на добавление записи.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.