JavaRush /Java Blog /Random-TL /Pagdaragdag ng database ng PostgreSQL sa isang RESTful na...
Artur
Antas
Tallinn

Pagdaragdag ng database ng PostgreSQL sa isang RESTful na serbisyo sa Spring Boot. Bahagi 2

Nai-publish sa grupo
Pagdaragdag ng database ng PostgreSQL sa isang RESTful na serbisyo sa Spring Boot. Bahagi 1 Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2 - 1 Kaya, sa huling bahagi natutunan namin kung paano mag-install ng isang database ng PostgresSQL sa isang computer, lumikha ng isang database sa pgAdmin, at lumikha at magtanggal din ng mga talahanayan dito nang manu-mano at programmatically. Sa bahaging ito ay muling isusulat namin ang aming programa upang matuto itong gumana sa database at mga talahanayan na ito. bakit tayo? Dahil ako mismo ay natututo sa iyo mula sa materyal na ito. At pagkatapos ay hindi lamang namin malulutas ang gawain sa kamay, kundi pati na rin iwasto ang mga error na lumitaw on the go, sa tulong ng payo mula sa mas may karanasan na mga programmer. Kaya naman, matututo tayong magtrabaho sa isang team ;) Una, gumawa tayo com.javarush.lectures.rest_exampleng bagong package sa isang folder at tawagan ito repository. Sa package na ito gagawa kami ng bagong interface 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>  {
}
Ang interface na ito ay "magically" na makikipag-ugnayan sa aming mga database at talahanayan. Bakit magically? Dahil hindi namin kakailanganing isulat ang pagpapatupad nito, at ibibigay ito sa amin ng Spring framework. Kailangan mo lang gumawa ng ganoong interface, at magagamit mo na ang "magic" na ito. Ang susunod na hakbang ay i-edit ang klase Clienttulad nito:
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
}
Ang ginawa lang namin sa klase na ito ay magdagdag lang ng ilang anotasyon. Dumaan tayo sa kanila:
  • @Entity - nagpapahiwatig na ang bean na ito (class) ay isang entity.
  • @Table - ipinapahiwatig ang pangalan ng talahanayan na ipapakita sa entity na ito.
  • @Id - column id (primary key - ang value na gagamitin para matiyak ang uniqueness ng data sa kasalukuyang table. Tandaan: Andrei )
  • @Column - nagsasaad ng pangalan ng column na nakamapa sa entity property.
  • @GeneratedValue - nagsasaad na ang property na ito ay bubuo ayon sa tinukoy na diskarte.
Ang mga pangalan ng mga patlang ng talahanayan ay hindi kailangang tumugma sa mga pangalan ng mga variable sa klase. Halimbawa, kung mayroon tayong variable firstName, pangalanan natin ang field sa table first_name. Ang mga anotasyong ito ay maaaring itakda nang direkta sa mga field at sa kanilang mga getter. Ngunit kung pipiliin mo ang isa sa mga pamamaraang ito, subukang panatilihin ang istilong ito sa iyong buong programa. Ginamit ko ang unang paraan upang paikliin ang mga listahan. Ang isang mas kumpletong listahan ng mga anotasyon para sa pagtatrabaho sa mga database ay matatagpuan dito . Ngayon ay pumunta tayo sa klase ClientServiceImplat muling isulat ito tulad ng sumusunod:
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;
    }
}
Tulad ng nakikita mo mula sa listahan, ang ginawa lang namin ay tanggalin ang mga linyang hindi na namin kailangan:
// Хранorще клиентов
private static final Map<Integer, Client>  CLIENT_REPOSITORY_MAP = new HashMap<>();

// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
Вместо них мы объявor наш интерфейс ClientRepository, а также поместor над ним аннотацию @Autowired, чтобы Spring автоматически добавил эту зависимость в наш класс. А также делегировали всю работу этому интерфейсу, а точнее его реализации, которую добавит Spring. Перейдем к заключительному и самому интересному этапу – тестированию нашего applications. Откроем программу Postman (How ей пользоваться смотрите здесь), И посылаем GET request по этому addressу: 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 request:
{
  "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: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
  Detail: Ключ "(id)=(1)" уже существует.
Посылаем еще раз такой же POST request, результат тот же, но с таким отличием: Ключ "(id)=(2)" уже существует. Посылаем в третий раз этот же request, и получаем Status: 201 Created. Посылаем еще раз GET request, и получаем на него ответ:
[
    {
        "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"
    }
]
Это говорит о том, что наша программа игнорирует тот факт, что эта table уже была предварительно заполнена, и назначает id опять начиная с единицы. What-ж, баг — это рабочий момент, не стоит отчаиваться, такое бывает часто. Поэтому я обращусь за помощью к более опытным коллегам: "Уважаемые коллеги, посоветуйте пожалуйста в комментариях How это пофиксить, чтобы программа заработала нормально". Помощь не заставила себя долго ждать, и Стас Пасинков подсказал мне в комментариях, в Howую сторону нужно посмотреть. Отдельное ему за это спасибо! А дело было в том, что в классе Client я неправильно указал стратегию для аннотации @GeneratedValue(strategy = GenerationType.IDENTITY) у поля id. Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2 - 3 Эта стратегия подходит для MySQL. Если же мы работаем с Oracle or 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;
What изменилось: поменялся тип столбца id нашей таблицы, но об этом чуть позже. Добавилась строчка снизу, в которой мы создаем новую последовательность clients_id_seq, указываем, что она должна начинаться с тройки (потому что последний id в файле populateDB.sql equals 2), и указываем, что инкремент должен происходить на единицу. Вернемся к типу столбца id. Здесь мы указали INTEGER, потому что если оставить SERIAL, то последовательность создастся автоматически, с тем же именем clients_id_seq, но будет начинаться с единицы (что и приводило к багу программы). Однако, теперь, если вы захотите удалить таблицу, то нужно будет дополнительно удалить и эту последовательность либо вручную через интерфейс pgAdmin, либо через файл .sql при помощи таких команд:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
Но если вы не используете для первоначального заполнения таблицы файл наподобие populateDB.sql, то для первичного ключа можно использовать типы SERIAL or BIGSERIAL, и не создавать последовательность вручную, соответственно и удалять ее отдельно не придется. Подробнее почитать о последовательностях можно на сайте оф. Документации PostgreSQL. Перейдем к annotationм поля id класса Client, и оформим их следующим образом:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
What мы сделали: установor новую аннотацию @SequenceGenerator для создания генератора последовательности, назначor ему Name clientsIdSeq, указали что это генератор для последовательности clients_id_seq, и добавor атрибут 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 для каждой новой сущности, перед тем How сохранить ее в БД, "сбегает" в БД за id. При значении > 1 (к примеру 5) - hibernate будет обращаться к БД за "новым" id реже (для примера - в 5 раз), при этом при обращении hibernate попросит БД зарезервировать это количество (в нашем случае 5) значений. Ошибка же которую вы описали говорит о том что hibernate хотел бы получить 50 дефолтных id, но вот в БД вы указали что готовы выдавать id для данной сущности только по 1-ой. Еще один баг был выловлен пользователем Nikolya Kudryashov: Если выполнить request из оригинальной статьи 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 строчки из file application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
В прошлый раз мы комментировали только последнюю строку, но т.к. мы уже создали и заполнor таблицу, то это мне показалось на данный момент более логичным. Перейдем к тестированию, выполним GET, POST, PUT и DELETE requestы через Postman, и увидим, что баги исчезли, и все работает нормально. Вот и всё, работа выполнена. Теперь можно подвести краткий итог, и рассмотреть, чему мы научorсь:
  • Устанавливать PostgreSQL на компьютер
  • Создавать базы данных в pgAdmin
  • Создавать и удалять таблицы вручную и программно
  • Заполнять таблицы через файлы .sql
  • Немного познакомorсь с «волшебным» интерфейсом JpaRepository каркаса Spring
  • Узнали о некоторых багах, которые могут возникнуть при создании такой программы
  • Поняли, что не нужно стесняться обращаться за советом к коллегам
  • Утвердorсь во мнении, что JavaRush комьюнити – это сила, которая всегда придет на помошь ;)
На этом пока можно и закончить.Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 2 - 4Спасибо всем, кто потратил свое время на прочтение данного материала. Буду рад вашим комментариям, замечаниям, дополнениям и конструктивной критике. Возможно, вы предложите более элегантные решения этой задачи, которые я обещаю добавить в эту статью посредством “ключевого слова” UPD, с упоминанием вас How автора, конечно. Ну и вообще, пишите, понравилась ли вам эта статья и такой стиль подачи материала, и вообще, стоит ли мне продолжать писать статьи на JR. Вот и дополнения подоспели: UPD1 : user Justinian настойчиво рекомендовал мне переименовать package com.javarush.lectures.rest_example в com.javarush.lectures.rest.example, и название проекта, чтобы не нарушать конвенции именования в Java. UPD2 user Александр Пьянов подсказал, что для инициализации поля ClientRepository в классе ClientServiceImpl лучше использовать конструктор чем аннотацию @Autowired. Объясняется это тем, что в редких случаях можно получить NullPointerException, и вообще, это является best practice, и я с ним соглашусь. По логике, если поле является обязательным для изначальной функциональности an object, то лучше его инизиализировать в конструкторе, ведь класс без конструктора не соберется в an object, следовательно, это поле поле будет проинициализировано еще на этапе создания an object. Добавлю фрагмент codeа с исправлениями (что на что нужно заменить):
@Autowired
private ClientRepository clientRepository;

private final ClientRepository clientRepository;

public ClientServiceImpl(ClientRepository clientRepository) {
   this.clientRepository = clientRepository;
}
Ссылка на первую часть: Добавляем БД PostgreSQL к RESTful сервису на Spring Boot. Часть 1 PS Если кто-то из вас захочет продолжить развивать это учебное приложение, то я буду рад добавить ссылку на ваши гайды в эту статью. Возможно, когда-нибудь эта программа вырастет во что-то похожее на настоящее бизнес-приложение, работу над которым вы сможете добавить в свое портфолио. PPS What касается этой скромной статьи, то я решил посвятить эту пробу пера нашим дорогим девушкам, женщинам и дамам. Кто знает, возможно сейчас и не было-бы в природе ни Java ни JavaRush, ни программирования, если бы не эта женщина. Поздравляю вас с праздником, дорогие вы наши умницы! С 8 Марта! Будьте счастливы и красивы!
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION