8 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

TDD приложений на Spring Boot: работа с базой данных

Энтерпрайз головного мозга

Блог о разработке приложений на Java EE и Spring

среда, 6 апреля 2016 г.

Разработка приложений со Spring Boot. Часть 2: Работа с базами данных

В своём предыдущем посте я описал начало разработки приложения с использованем Spring Boot. В этом посте я расскажу о работе с реляционными базами данных.

Spring Boot и реляционные БД

Из названия следует, что стандартный способ работы с реляционными базами данных реализован посредством Spring Data JPA. Основываясь на своём опыте, могу сказать, что возможности этого фреймворка и JPA полностью покрывают потребности практически любого проекта. Исключения составляют сложные запросы с вложенными запросами и кучей джоинов, но и они решаются при помощи хранимых процедур и представлений.
Теперь нашему проекту нужно добавить связь с базой данных. Для демонстрационного проекта вполне хватит и локальной БД вроде HSQLDB или H2:

В application.properties нужно добавить два свойства, что бы схема базы данных создавалась при запуске приложения:

Теперь всё готово для написания классов-сущностей и логики работы с базой данных.

Классы сущностей и репозитории

Для комментария к заявке создадим класс TicketComment:

При помощи JPA-аннотаций можно более подробно сконфигурировать классы сущностей и их маппинг в таблицы базы данных.

Теперь нам нужно определить два интерфейса для взаимодействия с базой данных. Для работы с заявками это будет TicketRepository:

Рассмотрим код описанного интерфейса более подробно:

  • Аннотация @Repository указывает, что данный класс или интерфейс является репозиторием и объект этого класса будет использоваться при внедрении зависимостей.
  • Описанный интерфейс расширяет CrudRepository, в котором уже определены основные CRUD-операции. Так же можно расширить интерфейс JpaRepository. Дженерики указывают, что данный репозиторий работает с сущностью Ticket, а классом первичных ключей во всех операциях является Long.
  • Метод findByResolveDateIsNull производит поиск всех записей сущности Ticket, у которых свойство resolveDate не задано.
  • Методы resolveTicket и reopenTicket обновляют запись, делая заявку закрытой или открытой. Для методов, которые модифицируют данные в БД необходимы аннотации @Transactional и @Modifying.

Реализацию данного интерфейса писать не нужно, за нас это сделает Spring Data JPA. Но если со стандартными методами, а так же с методами, с аннотацией @Query всё понятно, то как фреймворк определит поведение метода findByResolveDateIsNull? Всё просто, поведение метода реализуется на основании его названия:

  • Начало названия метода определяет, что нужно сделать в запросе. Например, findBy вернёт записи, соотвествующие критерии выборки (SELECT), deleteBy – удалит (DELETE).
  • Дальше указываются условия запроса. В приведённом примере условием является ResolveDateIsNull, что будет транслировано в resolve_date is null (на SQL). Если бы мы хотели просто произвести поиск по полному соответствию resolveDate, то сигнатура метода выглядела бы следующим образом: List findByResolveDate(Date date). Условия выборки можно объединять при помощи And или Or в зависимости от ситуации.

Методам репозитория так же могут передаваться объекты классов, определяющих дополнительные манипуляции при поиске. В указанном выше методе таким объектом является sort, добавляющий возможность изменения сортировки получаемых записей.
Метод, возвращающий несколько записей из БД может возвращать Iterable, Collection или даже Stream, здесь каждый решит за себя, что ему будет удобнее использовать.

Возможности Spring Data JPA серьёзно ускоряют и упрощают процесс разработки, позволяя меньше отвлекаться на написание кода работы с БД и сконцентрироваться на написании бизнес-логики. Но, что если запрос к БД имеет больше 2-3 условий, или условия выборки несколько сложнее? В этом случае удобным будет использование аннотации @Query, которая позволяет описывать запросы в JPQL или SQL (при установленном свойстве native). Рассмотрим на примере репозитория комментариев, TicketCommentRepository:

В свойстве value мы описали простой JPQL-запрос, в котором производится выборка всех TicketComment, у которых id вложенного объекта ticket соответствует искомому. Имена методов, у которых есть аннотация @Query, могут иметь свободную форму, Spring Data JPA реализует поведение этих методом на основании запроса.

По сути, оба описанных выше метода имеют одну и ту же функциональность – они возвращают список комментариев к указанной заявке.

На этом этапе у нас есть классы сущностей и репозитории с реализацией основных действий с БД. В следующем посте я опишу работу со Spring WebMVC на примере контроллеров и представлений с использованием Thymeleaf.

Работа с базой данных через Spring

Синопсис

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

Похожие посты

Содержание

Погнали!

База данных

База данных в этом примере такая же как в посте Работа с базой данных через JDBC. Там приводится ее структура и как ее поднять в MySQL. Для экономии места эти детали не привожу, в этом посте к базе данных добавится только хранимая функция.

Читать еще:  Redsnow не видит iphone в dfu

Структура проекта

Java код

Item.java

Provider.java

Warehouse.java

Уровень доступа к данным

Реализуем уровень доступа к данным тремя различными способами. Первый способ для объектов Item это использование спрингового вспомогательного класса JdbcTemplate который позволяет отправлять базе данных SQL-операторы любого типа для обновления или извлечения данных.
Второй способ для объектов Warehouse это использование похожего класса NamedParameterJdbcTemplate, который позволяет делать тоже самое, только используя параметризованные запросы, то есть если надо вставить в запрос данные, обычно это делается используя вопросики и затем с помощью класса PreparedStatement определялись их значения соблюдая последовательность, то в случае с параметризованными SQL-операторами мы просто определяем параметры в запросе основываясь на их именах, а не на последовательности.
Тертий способ для объектов Provider это использование спринговых классов, хранящих в себе SQL-операторы.

ItemDao.java

WarehouseDao.java

ProviderDao.java

Интерфейс ProviderSFDao предоставляет доступ к хранимой функции:

ProviderSFDao.java

Доступа к данным Item

В этом dao-классе, для запроса данных из базы будем использовать спринговый класс JdbcTemplate. Этот класс существенно упрощает программную модель при разработке логики доступа к данных с помощью JDBC.
В этом классе в методах запроса данных из базы, создается анонимный класс реализующий интерфейс RowMapper в котором мы реализуем метод mapRow. Этот метод трансформирует значение конкретной записи результирующего набора в необходимый объект предметной области.

ItemDaoImpl.java

В методах поиска findAll(), findById() и findByName() создается класс реализующий интерфейс RowMapper, который позволяет трансформировать результат запроса в объект предметной области.
Так же обратим внимание на метод insert() который добавляет запись в базу. Дело в том, что в записи, которая добавляется в базу, отсутствует значение в поле id, так как первичный ключ сгенерируется только после операции вставки, когда СУРБД сгенерирует значение идентифицирующее запись. Столбец ID объявлен с атрибутом AUTO_INCREMENT, это значит, что значение ему присваивается СУРБД. Для того, чтобы извлечь сгенерированные СУРБД ключи, для MySQL (и для H2) надо создать инстанс KeyHolder и передать его в метод update, а затем, после операции вставки извлечь значение из инстанса KeyHolder.

Доступа к данным Warehouse

WarehouseDaoImpl.java

Рассмотрим более подробно как работает метод findWarehousesWithItems(), еще раз приведу его код:

Чтобы вытащить все склады с вложенными в них товарами, нужно реализовать интерфейс org.springframework.jdbc.core.ResultSetExtractor в котором переопределяется метод extractData() для трансформации результирующего набора в список объектов Warehouse, который передается в качестве параметра в метод query класса org.springframework.jdbc.core.JdbcTemplate или, в нашем случае, в org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate, вместе со строкой запроса.
В этом методе из декартова произведения таблиц вытаскивается сначала запись warehouse а затем все записи item которые относятся к извлеченной записи warehouse. Это выглядит примерно так, допустим в базе есть три записи в таблице warehouses и пять записей в таблице items, причем к первой записи из таблицы warehouses относятся первые три записи из таблицы items, ко второй записи из таблицы warehouses относятся четвертая и пятая записи из таблицы items, а к третей записи из таблицы warehouses ничего не относится из таблицы items:

В результате запроса:

Декартово произведение таблиц, будет выглядеть так:

Затем, из получившегося декартова произведения удаляются ненужные записи. Ненужные записи это те у которых поле id из таблицы warehouses и поле warehouse_id из таблицы items не равны:

Окончательная таблица примет вид:

Заметим, что одна запись из таблицы warehouses (запись с айдишником 3) осталась нетронутой, хотя она не имеет соответствующих записей из таблицы items. Это потому что мы сделали левое объединение таблиц, то есть из левой таблицы берутся все записи, а оставляются только те, у которых совпадают айдишники либо те, у которых соответствующих записей из пристыковываемой таблицы нет.
Теперь рассмотрим, что происходит в методе extractMap() класса org.springframework.jdbc.core.ResultSetExtractor.
Результат выборки таблицы будет храниться в карте, где ключем будет айдишник из базы, а значением объект Warehouse.
Теперь пройдем шаг за шагом как происходит формирование карты.

  1. Сначала из результирующего набора берется первая запись, и вытаскивается из нее поле id, затем по этому айдишнику в карте ищется объект Warehouse. Так как его там нету, то создается новый объект Warehouse в котором просечиваются все поля из из первого из второго столбцов таблицы, то есть тех, которые относятся в складу и созданный объект Warehouse кладется в карту. После этого (но еще в той же самой итерации) из таблицы вытаскивается поле item_id, и если оно ноль или не NULL, то формируется объект Item оставшимися столбцами, который затем засовывается в созданный или вытащенный из карты объект Warehouse.
  2. На второй итерации, из результирующего набора опять вытаскивается айдишник, и по нему ищется объект Warehouse в карте, который, на этот раз, уже там есть. Ну а дальше формируется следующий объект Item который кладется в карту и так до тех пор пока не будет вытащенный новый айдшиник, для которого объекта Warehouse в карте еще нет. Затем создастся новый объект Warehouse который поместится в карту и дальше все повторяется.
Читать еще:  Прилуки. Новогодняя ночная площадь

Доступа к данным Provider

В классе доступа к данным ProviderDaoImpl реализуем логику доступа к данным в более объектно-ориентированной манере. То есть для каждой операции в базе данных, сохранение, поиск, обновление и пр. будем использовать целевой класс. Для этого вынесем всю логику запроса, трансформацию результирующего набора в объекты предметной области, из dao-класса в отдельный класс, который будет использоваться dao-классом, а тот отдельный класс, в свою очередь, будет наследоваться от спрингового класса, который реализует операции над данными JDBC.

ProviderDaoImpl.java

ProviderSFDaoImpl.java

Классы Spring, моделирующие операции JDBC

Класс наследуемый от класса SqlUpdate служит для обновления данных в базе. Этот класс хранит внутри себя SQL-оператор обновления, позволяет привязывать к запросу SQL-параметры, извлекать сгенерированный СУРБД ключ после вставки новой записи.

InsertProvider.java

Следующие два класса используют пакетные операции обновления наследуя спринговый класс BatchSqlUpdate. Работа с классом BatchSqlUpdate подобна SqlUpdate только добавляться записи в базу данных будут не по одной, а пачками. Это значит, что перед вставкой нескольких записей, мы их сначала накапливаем в массиве или в списке, а затем этот список передаем инстансу класса, наследуемому от класса BatchSqlUpdate который произведет пакетную вставку. Дальше в тестах, мы применим этот класс для добавления в базу поставщика со всеми деталями, которые он поставляет. Вот для добавления деталей понадобится пакетная вставка.

InsertItem.java

InsertItemProvider

UpdateProvider.java

Класс наследуемый от класса MappingSqlQuery предназначен для запроса данных. Метод mapRow() позволяет поместить результат запроса в класс-оболочку.

SelectAllProviders.java

SelectProviderById.java

DeleteProvider.java

Хранимые функции

Платформа Spring предоставляет несколько классов для упрощения запуска хранимых процедур или функций. Один из таких классов SqlFunction который позволяет вызвать SQL-функции в базе данных. Для начала создадим в базе данных хранимую функцию, которую мы затем вызовем.
Создайте sql-файл под названием store-function.sql в котором пропишем создание хранимой функции getNameById():

store-function.sql

Затем запустим этот скрипт в базе данных MySQL:

После этого в базе данных появится хранимая функция, и ее можно вызывать из джава кода. Для этого добавим класс, расширяющий класс SqlFunction в пакете com.dev.blogs.jdbc. Этот класс представлять операцию вызова хранимой функции:

SFNameById.java

Обращаться к этому классу будет DAO-класс ProviderSFDaoImpl, который будет вызываться из тестов.

Тесты

TestItem.java

TestProvider.java

TestWarehouse.java

Файлы конфигурации

В прошлом посте для получения соединения к базе данных мы создали свой источник данных, который оборачивал объект java.sql.DriverManager, который затем возвращал объект типа java.sql.Connection. Наш источник данных имеет один существенный недостаток — каждый раз когда нужно выполнить запрос к базе данных, создается новый объект Connection, то есть создается новое соединение с базой данных. В этот раз вместо своего источника данных мы воспользуемся спринговым org.apache.commons.dbcp.BasicDataSource который управляет набором Connection и следит за их оптимальным использованием, то есть создает несколько объектов Connection, помещает их в пул, и в случае надобности достает один из объектов Connection из пула и выдает приложению, а после завершения работы с ним, объект Connection не удаляется, а помещается обратно в пул, для повторного использования.
В файле datasource-dbcp.xml содержится та часть спрингового контекста в котором хранится конфигурация источника данных для базы данных MySQL.
Файл jdbc.properties добавляется в src/main/resources:

jdbc.properties

Файл datasource-dbcp.xml добавляется в src/main/resources/spring. В этот файл импортируется файл свойств jdbc.properties (строчка 15):

datasource-dbcp.xml

Общая конфигурация Spring добавляется в src/main/resources/spring. В этот файл импортируется файл datasource-dbcp.xml (строчка 15).

spring-context.xml

Сборка и запуск

pom.xml

Теперь запустим проект. Так как у нас только тесты, то тесты и будем запускать. Откройте командную строку и введите:

Результатом запуска тестов должно быть Tests run: 11, Failures: 0, Errors: 0, Skipped: 0.

Заметки разработчика

узнал сам – поделись с другими

3 февраля 2017 г.

Работа с базой данных в Spring Boot на примере postgresql

Актуальная версия статьи доступна на моём новом сайте devmark.ru.

Меня уже неоднократно просили написать продолжение статьи Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.

Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id. Сам профиль содержит кроме id также имя и фамилию. Поэтому создадим таблицу profiles в postgresql.

Читать еще:  Виртуальное хранилище файлов бесплатно

Для поля id можно использовать тип serial. Он представляет собой целое число, которое инкрементируется автоматически при вставке новой записи в таблицу.

При работе с БД нужно использовать пул подключений к БД, чтобы не создавать их заново при каждом новом sql-запросе, иначе выполнение запроса будет занимать продолжительное время. В качестве пула предлагаю использовать широко используемый в настоящий момент HikariCP. Также нам потребуется поддержка работы с БД со стороны Spring Boot и драйвер для работы с СУБД postgresql. Добавим все эти зависимости в наш проект.

При инициализации пула требуется указать параметры подключения к БД, такие как логин, пароль и т.п. Поскольку данные параметры являются изменяемыми и доступ к ним должен быть ограничен, вынесем их в отдельный текстовый файл и назовём его application.config. Пример содержимого такого файла:

mainPool.jdbcDriver=org.postgresql.Driver
mainPool.jdbcString=jdbc:postgresql://localhost:5432/database_name
mainPool.jdbcUser=username
mainPool.jdbcPassword=verySecretPassword

Чтобы Spring Boot увидел данные настройки, абсолютный путь к файлу следует указывать через параметр командной строки –spring.config.location=/путь/до/файла/application.config. Если запускаете проект при помощи Idea, указывайте данный параметр в строке Program Arguments.

Для удобства работы с этими настройками создадим новый класс ConnectionSettings, в который Spring автоматически подставит все настройки с префиксом “mainPool” в соответствующие поля, благодаря аннотации @ConfigurationProperties. Вообще это очень хорошая практика – группировать связанные настройки через префикс.

Для каждого из этих полей нужно создать геттер и сеттер, но я для краткости не стал их здесь приводить.

Наш пул подключений максимум может хранить до 5 объектов, однако это значение может быть переопределено через файл настроек.

Теперь создадим ещё один компонент, в котором будем инициализировать сам пул.

Аннотация @Bean позволяет нам вручную создавать бины, которые Spring потом сможет подставлять в другие компоненты.

Для работы с БД принято выделять отдельной слой DAO (data access object – объект доступа к данным). Интерфейс нашего слоя работы с профилями пользователей будет иметь следующий интерфейс:

Его реализация будет выглядеть так:

Обратите внимание, что ВСЕ dao-компоненты снабжаются аннотацией @Repository, которая является аналогом @Component. Также она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.

Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Обратите внимание, что для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет сделать нам запрос более безопасным с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон этого запроса.

NamedParameterJdbcTemplate – стандартный компонент, предоставляющий методы для взаимодействия с БД. ProfileMapper преобразует данные, полученные из БД в объект Profile. То есть он хранит в себе логику маппинга полей таблицы на поля класса. Более подробно мы рассмотрим его чуть ниже.

Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод query, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем типизированный список объектов Profile. Исходя из того, что id является первичным ключом в таблице и его значение уникально, наш список будет содержать единственный элемент с искомым профилем. Если же по данному id ничего не найдено – кидаем исключение, которое потом будет перехвачено в ErrorController (см. Spring Boot Restful Service).

Сам ProfileMapper не хранит внутреннего состояния и всего лишь реализует интерфейс RowMapper, типизированный нашим объектом Profile.

На вход он получает ResultSet, представляющий собой одну строку из выборки. Из этого ResultSet мы извлекаем значения полей благодаря методам getInt() и getString(), передавая им имя колонки в таблице.

Теперь осталось только внедрить наш ProfileDao в сервисный слой.

ProfileService, в свою очередь, вызывается из контроллера. То есть вырисовывается типичная трёхслойная архитектура, которой следует придерживаться при создании подобных приложений: контроллер (с аннотацией @Controller) -> сервис (@Service) -> dao (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а dao работает непосредственно с БД.

Теперь если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profile/1, то получите профиль с id = 1:

Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:

Если вам помог данный материал, порекомендуйте его другим пользователям и поставьте +1. Если у вас появились вопросы – пишите их в комментариях, постараюсь ответить.

голоса
Рейтинг статьи
Ссылка на основную публикацию
Статьи c упоминанием слов: