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

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

Работа с БД в Spring Boot на примере postgresql

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

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

CREATE TABLE public.profiles
(
id serial ,
first_name character varying ( 50 ) NOT NULL ,
last_name character varying ( 50 ) NOT NULL ,
age integer NOT NULL ,
CONSTRAINT profile_id_pk PRIMARY KEY (id)
);

insert into profiles (first_name, last_name, age) values ( ‘Иван’ , ‘Петров’ , 23 );

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

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

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

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

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

@Component
@ConfigurationProperties (prefix = “mainPool” )
public class ConnectionSettings <

private static int DEFAULT_MAX_POOL_SIZE = 5 ;

private String jdbcDriver;
private String jdbcString;
private String jdbcUser;
private String jdbcPassword;
private int jdbcMaxPoolSize = DEFAULT_MAX_POOL_SIZE ;
>

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

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

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

@Configuration
public class DatabaseConfig <

private final ConnectionSettings connectionSettings;

@Autowired
public DatabaseConfig(ConnectionSettings connectionSettings) <
this .connectionSettings = connectionSettings;
>

@Bean
public DataSource dataSource() <
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDriverClassName(connectionSettings.getJdbcDriver());
hikariConfig.setJdbcUrl(connectionSettings.getJdbcString());
hikariConfig.setUsername(connectionSettings.getJdbcUser());
hikariConfig.setPassword(connectionSettings.getJdbcPassword());
hikariConfig.setMaximumPoolSize(connectionSettings.getJdbcMaxPoolSize());
hikariConfig.setPoolName( “main” );
return new HikariDataSource(hikariConfig);
>
>

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

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

public interface ProfileDao <

getProfileById( int id);
>

Обратите внимание, что при поиске по id здесь мы будем возвращать типизированный Optional. То есть объект может быть в базе, а может и не быть. И в зависимости от кейса это может трактоваться как ошибка, так и нормальное поведение. Решение о том, ошибка это или нет, будет принимать сервисный слой, который мы рассмотрим далее.

Реализация класса Profile предельно проста. Его единственное назначение – это отображать поля таблицы в поля класса на Java. Для краткости не буду приводить код всего класса, ибо он достаточно прост.

public class Profile <

private int id;
private String firstName;
private String lastName;
private int age;

public int getId() <
return id;
>

public void setId( int id) <
this .id = id;
>

// далее идут остальные get- и set-методы.

@Repository
public class ProfileDaoImpl implements ProfileDao <

private static final String SQL_GET_PROFILE_BY_ID =
“select id, first_name, last_name, age from profiles where id = :id” ;

private final ProfileMapper profileMapper;
private final NamedParameterJdbcTemplate jdbcTemplate;

@Autowired
public ProfileDaoImpl(
ProfileMapper profileMapper,
NamedParameterJdbcTemplate jdbcTemplate
) <
this .profileMapper = profileMapper;
this .jdbcTemplate = jdbcTemplate;
>

@Override
public Optional

getProfileById( int id) <
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue( “id” , id);
try <
return Optional.ofNullable(
jdbcTemplate.queryForObject(
SQL_GET_PROFILE_BY_ID ,
params,
profileMapper
)
);
> catch (EmptyResultDataAccessException e) <
return Optional.empty();
>
>
>

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

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

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

Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод queryForObject, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем объект Profile или исключение EmptyResultDataAccessException если объект не найден. Исходя из того, что id является первичным ключом в таблице и его значение уникально, мы можем здесь использовать метод queryForObject(). Если бы искали не по уникальному значению, то использовали бы метод query(), который возвращает список объектов. Результат оборачиваем в Optional.

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

@Override
public Profile mapRow(ResultSet rs, int rowNum) throws SQLException <
Profile profile = new Profile();
profile.setId(rs.getInt( “id” ));
profile.setFirstName(rs.getString( “first_name” ));
profile.setLastName(rs.getString( “last_name” ));
profile.setAge(rs.getInt( “age” ));
return profile;
>
>

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

Теперь осталось только внедрить наш ProfileDao в сервисный слой. В предыдущей статье мы уже создавали реализацию сервисного слоя ProfileServiceMock, которая является заглушкой и на самом деле ни в какую базу не ходит. Сейчас мы создадим другую реализацию того же сервиса:

@Primary
@Service
public class ProfileServiceImpl implements ProfileService <

private final ProfileDao profileDao;

@Autowired
public ProfileServiceImpl(ProfileDao profileDao) <
this .profileDao = profileDao;
>

@Override
public Profile getProfile( int personId) <
return profileDao.getProfileById(personId)
.orElseThrow(() -> new ProfileNotFoundException(personId));
>
>

Обратите внимание на аннотацию @Primary. Если её не указывать, то спринг не сможет заинжектить в ProfileController нужную нам реализацию сервиса, т.к. по факту у нас их две. Чтобы указать, что по умолчанию нам нужна именно эта реализация, мы и используем данную аннотацию.

Как я уже говорил, именно сервисный слой находится в контексте выполнения запроса и может правильно трактовать пустой результат из dao. В данном случае это ошибка и здесь Optional предоставляет очень удобный метод orElseThrow(), в который мы передаём наше исключение через лямбда-выражение.

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

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

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

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

Итоги

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

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

Синопсис

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

Читать еще:  Dark Reader: добавляем "ночной режим" в Chrome

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

Содержание

Погнали!

База данных

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

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

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:

Читать еще:  Как стереть все данные с iphone 5?

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.

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

Блог о разработке приложений на 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.

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

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

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. Если у вас появились вопросы – пишите их в комментариях, постараюсь ответить.

Spring Data JPA. Работа с БД. Часть 1

Spring – достаточно многофункциональный framework, и если вы надумали делать enterprise проект, то вам не обойтись без возможности работать с базами данных. В этой серии уроков, мы научимся работать с БД используя возможности Spring Data.

Шаг 0. Постановка задачи

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

Нашей задачей будет: написать банковскую систему управления клиентами и персоналом. Может звучит немного и сложно, но на самом деле все довольно просто.

Давайте посмотри на рисунок ниже:

Примерно так выглядит облегченная модель банковской системы.

Шаг 1. Моделируем ERD

Теперь нужно на основании построенной модели выше на изображении, смоделировать ERD диаграмму.

ERD (Entity-Relationship Diagram) – это отображение модели сущностей в базе данных. Попросту мы моделируем сущности (таблицы) базы данных для определенной модели.

Вот такая ERD получилась у меня для нашей модели Банковской Системы.

Для построения ERD я использую сервис lucidchart.com

Теперь немного разберем нашу ERD:

worker – таблица, которая будет хранить все сотрудников определенного банка.

bank – таблица, которая будет хранить банки системы.

client – таблица клиентов банка, или нескольких банков.

bank_of_account – таблица, которая будет хранить счета клиентов банка.

Как вы видите между таблицами есть связи, о них вы можете почитать тут – Как связать Entity в JPA?

Шаг 2. Создание проекта и добавление Dependencies

Создаем новый проект и называем его com.devcolibri.dataexam.

И pom.xml будет иметь следующие зависимости:

Как вы уже могли заметить использую я MySQL поэтому и зависимость подключил именно для этой БД.

Если вы захотите использовать другую БД, то ничего вам не мешает поменять зависимость и данные в файле свойств.

Шаг 3. Конфигурация проекта

Теперь нам нужно сконфигурировать проект, а именно Spring Configuration.

Для начало посмотрите на текущую структуру проекта ниже и создайте себе такую же.

В resources есть файл app.properties в нем хранятся все свойства, необходимые проекта, в нашем случае это доступ к БД и т.п.

Данные свойства мы будем использовать в конфигурации EntityManager ниже.

Теперь обратите своё внимание на пакет configuration, в класс AppInitializer добавим следующее содержимое:

Я выделил 9-ю строку, так как в ней мы регистрируем конфигурацию необходимых нам бинов для работы с БД.

А теперь давайте создадим саму конфигурацию DataConfig, которая будет инлдектить JPA бины:

В дальнейшем я еще буду ссылаться на конфигурацию выше, для разъяснения некоторых вещей с этой конфигурации.

@Configuration – говорит, что данный класс является Spring конфигурацией;

@EnableTransactionManagement – включает TransactionManager для управления транзакциями БД;

@ComponentScan(“com.devcolibri.dataexam”) – указываем Spring где нужно искать Entity, DAO, Service и т.п.;

@PropertySource(“classpath:app.properties”) – подключаем файл свойств созданный выше;

@EnableJpaRepositories(“com.devcolibri.dataexam.repository”) – включаем возможность использования JPARepository и говорим, где их искать. (будем рассматривать позже детальней).

В нашем случае нужен для возможности получать свойства из property файла.

Шаг 4. Создаем Entity на основе нашей ERD диаграммы

Начнем с Банка (Bank):

Подробней о JPA можно почитать тут.

Теперь создаем BankAccount:

Как вы уже заметили BankAccount использует Client, поэтому создаем Client entity:

Ну и рабочие (Worker):

У каждого рабочего есть своя должность, эти должности мы положим в enum WorkerStatus:

Как работать с Enums в JPA я показывал тут.

Разработка приложений со Spring. Базовое веб-приложение.

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

Содержание

  1. О проекте, его структура и зависимости
  2. Классы-сущности и репозитории
  3. Модульное и интеграционное тестирование
  4. Контроллеры
  5. Шаблоны Thymeleaf
  6. Интернационализация

О проекте

Разрабатываемый проект — простой хелпдеск, основная цель которого — предоставить пользователям возможность уведомлять службу технической поддержки о возникающих проблемах. Кроме возможности создания самих заявок (или обращений), должна быть реализована возможность оставлять комментарии к заявкам.

Для данного проекта потребуются следующие зависимости:

  • Spring Data JPA для работы с базой данных
  • Thymeleaf для описания шаблонов
  • СУБД H2 в качестве хранилища данных
  • Lombok для минимизации рутинного кода вроде get/set-методов и конструкторов

Project Lombok

Данный инструмент значительно упрощает процесс разработки, добавляя перед компиляцией необходимые рутинные элементы, такие как get/set-методы, конструкторы, логгеры и многое другое. Поддержка данного инструмента есть во всех главных Java IDE. Более подробно с Lombok можно ознакомиться на официальном сайе проекта.

В итоге pom.xml будет выглядеть следующим образом:

Поскольку проект основан на Spring Boot, можно сгеренировать заготовку проекта при помощи Spring Initializr.

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

Как уже было сказано, основные возможности нашего проекта — создание и обсуждение заявок. Следовательно, нам потребуется всего два класса-сущностей: для описания заявки и для описания комментария. Авторизация будет рассматриваться в следующей статье, так что в классах-сущностях информации об авторах записей не будет.

В классе, описывающем заявку или обращение, понадобится четыре свойства:

  • Идентификатор
  • Краткое описание проблемы
  • Подробное описание проблемы
  • Дата создания

В классе, описывающем комментарий к заявке, понадобятся также четыре свойства:

  • Идентификатор
  • Текст комментария
  • Дата создания
  • Ссылка на заявку

Немножко задержимся на аннотациях, фигурирующих в наших классах-сущностях.

  • @Entity — JPA-аннотация уровня типа, обозначающая, что данный класс проецируется на таблицу в базе данных.
  • @Data — аннотация Lombok, добавляющая все рутинные методы классов сущностей, среди которых get/set-методы, hashCode, equals и toString.
  • @NoArgsConstructor добавляет конструктор без аргументов.
  • @AllArgsConstructor добавляет конструктор со всеми аргументами.
  • @Id означает, что помеченное свойство является первичным ключом.
  • @GeneratedValue означает, что значение данного свойства при сохранении должно быть сгенерировано. В Spring Boot по-умолчанию используется стратегия IDENTITY. Таким образом фактическое значение данной аннотации: @GeneratedValue(strategy = GenerationType.IDENTITY)
  • @Column указывает дополнительные свойства колонки в таблице БД. Атрибут nullable=false указывает фреймворку, что свойство не может быть null, атрибут updatable=false — что свойство не может изменяться, а columnDefinition=»TEXT» — что тип колонки должен быть TEXT. Это важно, так как в некоторых колонках нужно хранить более 255 символов.
  • @Temporal указывает, что свойство должно храниться в таблице БД как дата.
  • @ManyToOne указывает на связанный объект. Для связи в таблице БД должна существовать колонка вида имя_свойства_id, в которой будет храниться первичный ключ связанной записи. В нашем случае имя колонки будет ticket_id.

Структура базы данных

Обратите внимание, что создавать отдельно структуру базы данных не нужно, это за нас сделает Spring Data JPA, так как мы будем использовать СУБД H2, которая расценивается связкой Spring Boot и Spring Data JPA как тестовая. Если есть необходимость автоматического создания структуры базы данных в полноценных СУБД, вроде PostgreSQL или MySQL, то это можно настроить в свойствах приложения. Однако данный подход не рекомендуется и является опасным при применении в рабочих решениях. Более подробно об этом рассказано в документации Spring Boot.

Для доступа к данным нам потребуются репозитории. Поскольку в нашем проекте может понадобится постраничная разбивка данных (пейджинация), мы будем использовать PagingAndSortingRepository.

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