JavaRush /Java блог /Random UA /Додаємо БД PostgreSQL до RESTful сервісу на Spring Boot. ...
Artur
40 рівень
Tallinn

Додаємо БД PostgreSQL до RESTful сервісу на Spring Boot. Частина 2

Стаття з групи Random UA
Додаємо БД PostgreSQL до RESTful сервісу на Spring Boot. Частина 1 Додаємо БД PostgreSQL до RESTful сервісу на Spring Boot.  Частина 2 - 1 Отже, в минулому ми навчабося встановлювати базу даних PostgresSQL на комп'ютер, створювати БД в pgAdmin, а також створювати і видаляти в ній таблиці вручну і програмно. У цій частині ми будемо переписувати нашу програму, щоб вона навчилася працювати з цією БД та таблицями. Чому ми? Тому що я і сам навчаюсь разом з вами на цьому матеріалі. І далі ми будемо не тільки вирішувати поставлене завдання, а й виправляти помилки, що виникають на ходу, за допомогою порад більш досвідчених програмістів. Так би мовити, вчитимемося працювати в команді ;) Для початку створимо в папці com.codegym.lectures.rest_exampleновий пакет, і назвемо його repository. У цьому пакеті створимо новий інтерфейс ClientRepository:
package com.codegym.lectures.rest_example.repository;

import com.codegym.lectures.rest_example.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ClientRepository extends JpaRepository<Client, Integer>  {
}
Цей інтерфейс і буде "чарівним чином" взаємодіяти з нашими базами даних та таблицями. Чому чарівним чином? Тому що його реалізацію нам писати буде не потрібно, а її надасть каркас Spring. Достатньо тільки створити такий інтерфейс, і вже можна скористатися цією «магією». Наступним кроком відредагуємо клас Clientтаким чином:
package com.codegym.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.codegym.lectures.rest_example.service;

import com.codegym.lectures.rest_example.model.Client;
import com.codegym.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.codegym.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.codegym.lectures.rest_exampleв com.codegym.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 березня! Будьте щасливі та красиві!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ