Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 1
Итак, в прошлой части мы научились устанавливать базу данных PostgresSQL на компьютер, создавать БД в pgAdmin, а также создавать и удалять в ней таблицы вручную и программно.
В этой части мы будем переписывать нашу программу, чтобы она научилась работать с этой БД и таблицами.
Почему мы? Потому что я и сам учусь вместе с вами на этом материале. И дальше мы будем не только решать поставленную задачу, но и исправлять возникающие ошибки на ходу, при помощи советов более опытных программистов. Так сказать, будем учиться работать в команде ;)
Для начала создадим в папке
com.javarush.lectures.rest_example
новый пакет, и назовем его repository
.
В этом пакете создадим новый интерфейс ClientRepository
:
package com.javarush.lectures.rest_example.repository;
import com.javarush.lectures.rest_example.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClientRepository extends JpaRepository<Client, Integer> {
}
Этот интерфейс и будет "волшебным образом" взаимодействовать с нашими базами данных и таблицами. Почему волшебным образом?
Потому что его реализацию нам писать будет не нужно, а ее предоставит нам каркас Spring. Достаточно только создать такой интерфейс, и уже можно пользоваться этой «магией».
Следующим шагом отредактируем класс Client
таким образом:
package com.javarush.lectures.rest_example.model;
import javax.persistence.*;
@Entity
@Table(name = "clients")
public class Client {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
@Column(name = "phone")
private String phone;
//… getters and setters
}
Все что мы сделали в этом классе — это просто добавили некоторые аннотации.
Пройдемся по ним:
- @Entity — указывает, что данный бин (класс) является сущностью.
- @Table — указывает на имя таблицы, которая будет отображаться в этой сущности.
- @Id — id колонки (первичный ключ - значение которое будет использоваться для обеспечения уникальности данных в текущей таблице прим. Andrei)
- @Column — указывает на имя колонки, которая отображается в свойство сущности.
- @GeneratedValue — указывает, что данное свойство будет создаваться согласно указанной стратегии.
firstName
, то поле в таблице мы назовем first_name
.
Эти аннотации можно устанавливать как непосредственно у полей, так и у их геттеров. Но если вы выбрали один из этих способов, то постарайтесь придерживаться этого стиля во всей вашей программе. Я использовал первый способ, просто чтобы сократить листинги.
Более полный список аннотаций для работы с базами данных можно найти здесь.
Теперь перейдем к классу ClientServiceImpl
и перепишем его следующим образом:
package com.javarush.lectures.rest_example.service;
import com.javarush.lectures.rest_example.model.Client;
import com.javarush.lectures.rest_example.repository.ClientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ClientServiceImpl implements ClientService {
@Autowired
private ClientRepository clientRepository;
@Override
public void create(Client client) {
clientRepository.save(client);
}
@Override
public List<Client> readAll() {
return clientRepository.findAll();
}
@Override
public Client read(int id) {
return clientRepository.getOne(id);
}
@Override
public boolean update(Client client, int id) {
if (clientRepository.existsById(id)) {
client.setId(id);
clientRepository.save(client);
return true;
}
return false;
}
@Override
public boolean delete(int id) {
if (clientRepository.existsById(id)) {
clientRepository.deleteById(id);
return true;
}
return false;
}
}
Как видно из листинга, всё что мы сделали, это удалили уже не нужные нам строки:
// Хранилище клиентов
private static final Map<Integer, Client> CLIENT_REPOSITORY_MAP = new HashMap<>();
// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
Вместо них мы объявили наш интерфейс ClientRepository
, а также поместили над ним аннотацию @Autowired, чтобы Spring автоматически добавил эту зависимость в наш класс.
А также делегировали всю работу этому интерфейсу, а точнее его реализации, которую добавит Spring.
Перейдем к заключительному и самому интересному этапу – тестированию нашего приложения. Откроем программу Postman (как ей пользоваться смотрите здесь),
И посылаем GET запрос по этому адресу: http://localhost:8080/clients.
Получаем такой ответ:
[
{
"id": 1,
"name": "Vassily Petrov",
"email": "vpetrov@jr.com",
"phone": "+7 (191) 322-22-33)"
},
{
"id": 2,
"name": "Pjotr Vasechkin",
"email": "pvasechkin@jr.com",
"phone": "+7 (191) 223-33-22)"
}
]
Посылаем POST запрос:
{
"name" : "Amigo",
"email" : "amigo@jr.com",
"phone" : "+7 (191) 746-43-23"
}
И… ловим наш первый баг в программе:
{
"timestamp": "2020-03-06T13:21:12.180+0000",
"status": 500,
"error": "Internal Server Error",
"message": "could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
"path": "/clients"
}
Просматриваем логи, и обнаруживаем там такую ошибку:
org.postgresql.util.PSQLException: ОШИБКА: повторяющееся значение ключа нарушает ограничение уникальности "clients_pkey"
Detail: Ключ "(id)=(1)" уже существует.
Посылаем еще раз такой же POST запрос, результат тот же, но с таким отличием: Ключ "(id)=(2)" уже существует.
Посылаем в третий раз этот же запрос, и получаем Status: 201 Created.
Посылаем еще раз GET запрос, и получаем на него ответ:
[
{
"id": 1,
"name": "Vassily Petrov",
"email": "vpetrov@jr.com",
"phone": "+7 (191) 322-22-33)"
},
{
"id": 2,
"name": "Pjotr Vasechkin",
"email": "pvasechkin@jr.com",
"phone": "+7 (191) 223-33-22)"
},
{
"id": 3,
"name": "Amigo",
"email": "amigo@jr.com",
"phone": "+7 (191) 746-43-23"
}
]
Это говорит о том, что наша программа игнорирует тот факт, что эта таблица уже была предварительно заполнена, и назначает id опять начиная с единицы.
Что-ж, баг — это рабочий момент, не стоит отчаиваться, такое бывает часто. Поэтому я обращусь за помощью к более опытным коллегам: "Уважаемые коллеги, посоветуйте пожалуйста в комментариях как это пофиксить, чтобы программа заработала нормально".
Помощь не заставила себя долго ждать, и Стас Пасинков подсказал мне в комментариях, в какую сторону нужно посмотреть.
Отдельное ему за это спасибо!
А дело было в том, что в классе Client
я неправильно указал стратегию для аннотации @GeneratedValue(strategy = GenerationType.IDENTITY)
у поля id
.
Эта стратегия подходит для MySQL. Если же мы работаем с Oracle или PostrgeSQL, то нужно установить другую стратегию.
Подробнее о стратегиях для первичных ключей можно почитать здесь.
Я выбрал стратегию GenerationType.SEQUENCE. Для ее реализации нам нужно будет немного переписать файл initDB.sql, и, конечно аннотации поля id класса Client.
Переписываем initDB.sql:
CREATE TABLE IF NOT EXISTS clients
(
id INTEGER PRIMARY KEY ,
name VARCHAR(200) NOT NULL ,
email VARCHAR(254) NOT NULL ,
phone VARCHAR(50) NOT NULL
);
CREATE SEQUENCE clients_id_seq START WITH 3 INCREMENT BY 1;
Что изменилось: поменялся тип столбца id нашей таблицы, но об этом чуть позже.
Добавилась строчка снизу, в которой мы создаем новую последовательность clients_id_seq, указываем, что она должна начинаться с тройки (потому что последний id в файле populateDB.sql равен 2), и указываем, что инкремент должен происходить на единицу.
Вернемся к типу столбца id. Здесь мы указали INTEGER, потому что если оставить SERIAL, то последовательность создастся автоматически, с тем же именем clients_id_seq, но будет начинаться с единицы (что и приводило к багу программы).
Однако, теперь, если вы захотите удалить таблицу, то нужно будет дополнительно удалить и эту последовательность либо вручную через интерфейс pgAdmin, либо через файл .sql при помощи таких команд:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
Но если вы не используете для первоначального заполнения таблицы файл наподобие populateDB.sql, то для первичного ключа можно использовать типы SERIAL или BIGSERIAL, и не создавать последовательность вручную, соответственно и удалять ее отдельно не придется.
Подробнее почитать о последовательностях можно на сайте оф. Документации PostgreSQL.
Перейдем к аннотациям поля id
класса Client
, и оформим их следующим образом:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
Что мы сделали: установили новую аннотацию @SequenceGenerator
для создания генератора последовательности, назначили ему имя clientsIdSeq
, указали что это генератор для последовательности clients_id_seq
, и добавили атрибут allocationSize = 1
Это необязательный атрибут, но если мы этого не сделаем, то при запуске программы получим такую ошибку:
org.hibernate.MappingException: The increment size of the [clients_id_seq] sequence is set to [50] in the entity mapping while the associated database sequence increment size is [1]
Вот что пишет user Andrei по этому поводу в комментариях:
allocationSize в первую очередь предназначен для сокращения похода hibernate-ом в БД за "новым id". При значении == 1 - hibernate для каждой новой сущности, перед тем как сохранить ее в БД, "сбегает" в БД за id. При значении > 1 (к примеру 5) - hibernate будет обращаться к БД за "новым" id реже (для примера - в 5 раз), при этом при обращении hibernate попросит БД зарезервировать это количество (в нашем случае 5) значений. Ошибка же которую вы описали говорит о том что hibernate хотел бы получить 50 дефолтных id, но вот в БД вы указали что готовы выдавать id для данной сущности только по 1-ой.
Еще один баг был выловлен пользователем Nikolya Kudryashov:
Если выполнить запрос из оригинальной статьи http://localhost:8080/clients/1 ,То вернётся ошибка:
{
"timestamp": "2020-04-02T19:20:16.073+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.javarush.lectures.rest_example.model.Client$HibernateProxy$ZA2m7agZ[\"hibernateLazyInitializer\"])",
"path": "/clients/1"
}
Эта ошибка связана с ленивой инициализацией Hibernate, и чтобы от нее избавиться, нам необходимо добавить в класс Client дополнительную аннотацию :
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
Таким вот образом:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
Теперь запустим нашу программу (предварительно удалив таблицу clients из БД, если она осталась там с прошлого раза), и закомментируем 3 строчки из файла application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
В прошлый раз мы комментировали только последнюю строку, но т.к. мы уже создали и заполнили таблицу, то это мне показалось на данный момент более логичным.
Перейдем к тестированию, выполним GET, POST, PUT и DELETE запросы через Postman, и увидим, что баги исчезли, и все работает нормально.
Вот и всё, работа выполнена.
Теперь можно подвести краткий итог, и рассмотреть, чему мы научились:
- Устанавливать PostgreSQL на компьютер
- Создавать базы данных в pgAdmin
- Создавать и удалять таблицы вручную и программно
- Заполнять таблицы через файлы .sql
- Немного познакомились с «волшебным» интерфейсом JpaRepository каркаса Spring
- Узнали о некоторых багах, которые могут возникнуть при создании такой программы
- Поняли, что не нужно стесняться обращаться за советом к коллегам
- Утвердились во мнении, что JavaRush комьюнити – это сила, которая всегда придет на помошь ;)
com.javarush.lectures.rest_example
в com.javarush.lectures.rest.example
, и название проекта, чтобы не нарушать конвенции именования в Java.
UPD2 user Александр Пьянов подсказал, что для инициализации поля ClientRepository
в классе ClientServiceImpl
лучше использовать конструктор чем аннотацию @Autowired
.
Объясняется это тем, что в редких случаях можно получить NullPointerException
, и вообще, это является best practice, и я с ним соглашусь.
По логике, если поле является обязательным для изначальной функциональности объекта, то лучше его инизиализировать в конструкторе, ведь класс без конструктора не соберется в объект, следовательно, это поле поле будет проинициализировано еще на этапе создания объекта.
Добавлю фрагмент кода с исправлениями (что на что нужно заменить):
@Autowired
private ClientRepository clientRepository;
private final ClientRepository clientRepository;
public ClientServiceImpl(ClientRepository clientRepository) {
this.clientRepository = clientRepository;
}
Ссылка на первую часть: Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 1
PS Если кто-то из вас захочет продолжить развивать это учебное приложение, то я буду рад добавить ссылку на ваши гайды в эту статью. Возможно, когда-нибудь эта программа вырастет во что-то похожее на настоящее бизнес-приложение, работу над которым вы сможете добавить в свое портфолио.
PPS Что касается этой скромной статьи, то я решил посвятить эту пробу пера нашим дорогим девушкам, женщинам и дамам. Кто знает, возможно сейчас и не было-бы в природе ни Java ни JavaRush, ни программирования, если бы не эта женщина.
Поздравляю вас с праздником, дорогие вы наши умницы!
С 8 Марта! Будьте счастливы и красивы!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ