JavaRush /Java блог /Java Developer /Добавляем БД PostgreSQL к RESTful сервису на Spring Boot....
Artur
40 уровень
Tallinn

Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2

Статья из группы Java Developer
Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 1 Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2 - 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"
}
Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2 - 2 Просматриваем логи, и обнаруживаем там такую ошибку:

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. Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2 - 3 Эта стратегия подходит для 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 комьюнити – это сила, которая всегда придет на помошь ;)
На этом пока можно и закончить.Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2 - 4Спасибо всем, кто потратил свое время на прочтение данного материала. Буду рад вашим комментариям, замечаниям, дополнениям и конструктивной критике. Возможно, вы предложите более элегантные решения этой задачи, которые я обещаю добавить в эту статью посредством “ключевого слова” UPD, с упоминанием вас как автора, конечно. Ну и вообще, пишите, понравилась ли вам эта статья и такой стиль подачи материала, и вообще, стоит ли мне продолжать писать статьи на JR. Вот и дополнения подоспели: UPD1 : user Justinian настойчиво рекомендовал мне переименовать package 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 Марта! Будьте счастливы и красивы!
Комментарии (31)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #2887310 Уровень 3
8 мая 2023
А кто то переходил по ссылке чтобы почитать о стратегиях для первичных ключей?Я бы на месте автора отредачил бы статью а то после клика по ссылке немного мысли не в то русло уходят
Victor Уровень 2
22 апреля 2023
implements ClientService не описан интерфейс ClientService
14 сентября 2022
Если выполнить GET с несуществующим id, то выходит "500 Internal Server Error", а не "404 Not Found", как было задумано.
7 марта 2022
Как приятно читать ваши статьи! Чувствуется желание делиться знаниями, помогать, учить и учиться самому. Всё очень понравилось, четко, здорово, что можно проследить ход мысли. Пожалуйста, не останавливайтесь, пишите ещё. Если есть какие-то другие каналы, где вы выступаете в роли преподавателя, с удовольствием подпишусь на вас:)
15 января 2022
А как выполняется вставка даты через Spring?
jimaltair Уровень 41
8 июля 2021
Сделал варник, задеплоил в Томкэт. Ничего не работает. Если у кого-то получилось, будьте добры, поделитесь опытом. p.s. Возможно, это так не работает, и нужно дописать/переписать какой-то функционал у приложения? Прошу сильно не ругаться, только начал изучать веб-программирование по первым туториалам
Valentin Уровень 0
30 декабря 2020
Спасибо Вам за статью. Именно благодаря ей, я всё же смог добиться последовательного добавления Id
Ihor Уровень 41
3 декабря 2020
Огромное СПАСИБО!!!
Василий Бабин Уровень 28 Expert
8 ноября 2020
Спасибо большое! Продолжайте писать.
Ols Уровень 23
25 октября 2020
Спасибо за статью, интересно и полезно!