Статьи
YouTube-канал
Отзывы

CrudRepository в Spring Data

Исходники

26 мая 2018

Тэги: Hibernate, Java 8, PostgreSQL, rest, Spring, Spring Boot, Spring Data.

Содержание

  1. Получение всех записей
  2. Поиск по id и по имени
  3. Добавление и удаление записи
  4. Удаление записи
  5. Итоги

В статье Hibernate и Spring Boot мы рассматривали использование Hibernate для того, чтобы не писать sql-запросы в слое доступа к данным. Сегодня мы пойдём ещё дальше и рассмотрим, как Spring Data может генерировать за вас сам слой доступа к данным со всеми методами, которые вам нужны в сервисном слое.

В качестве примера возьмём сущность «Страна» с её названием в качестве единственного параметра и на примере этой сущности шаг за шагом создадим все необходимые операции для поиска, добавления, редактирования и удаления этой сущности. В СУБД postgres надо создать следующую таблицу:

CREATE TABLE country
(
  id serial,
  name character varying(50) NOT NULL,
  CONSTRAINT country_id_pk PRIMARY KEY (id)
);

Теперь создадим типовой maven-проект и добавим в pom.xml необходимые зависимости. Полную версию файла можно посмотреть на github.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.12.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.2.jre7</version>
    </dependency>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.0</version>
    </dependency>
</dependencies>

spring-boot-starter-web отвечает за обработку http-запросов, а spring-boot-starter-data-jpa предоставляет функционал доступа к данным. Также мы добавляем драйвер для работы с целевой СУБД.

Точка запуска приложения имеет стандартный для Spring Boot вид:

@SpringBootApplication(scanBasePackages = "ru.devmark")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Параметр scanBasePackages говорит Spring, какие пакеты сканировать для поиска и создания бинов.

Начнём со слоя доступа к данным. В Hibernate сущность, которая мапится на нашу таблицу, будет выглядеть так:

@Entity
@Table(name = "country")
public class Country {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    // далее идут get- и set-методы...
}

@Entity говорит о том, что это - класс-сущность. @Table позволяет в явном виде задать имя таблицы в БД, но если имя класса совпадает с названием таблицы, то эту аннотацию можно не указывать. @Id указывает на поле, которое будет идентификатором сущности. В таблице это поле должно быть помечено как primary key. @GeneratedValue позволяет задать способ генерации этого идентификатора. В нашем случае мы используем то значение, которое будет присваиваться в БД. Поле name совпадает с названием поля в базе, поэтому явно имя колонки указывать не требуется.

Spring Data предоставляет абстракцию CrudRepository, которая типизируется целевой сущностью и её id. CrudRepository имеет набор базовых методов для работы с сущностью, названия которых говорят сами за себя:

<S extends T> S save(S var1);

<S extends T> Iterable<S> save(Iterable<S> var1);

T findOne(ID var1);

boolean exists(ID var1);

Iterable<T> findAll();

Iterable<T> findAll(Iterable<ID> var1);

long count();

void delete(ID var1);

void delete(T var1);

void delete(Iterable<? extends T> var1);

void deleteAll();

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

public interface CountryDao extends CrudRepository<Country, Integer> {

    List<Country> findAllByOrderByIdDesc();

    Country findByName(String name);
}

Для того, чтобы Spring создал реализацию этих методов, требуется соблюдать определённый порядок именования. Хоть в базовом интерфейс уже имеется метод findAll(), мы хотим задать сортировку элементов так, чтобы более поздние записи (с большим id) оказывались в начале списка. Для этого называем наш новый метод findAllByOrderByIdDesc(). Также мы хотим искать страну не только по id, но и по её имени, поэтому добавляем метод findByName().

Как уже упоминалось выше, достаточно создать интерфейс без реализации, назвать новые методы определённым образом и мы уже можем подставлять наше dao в сервисный слой при помощи @Autowired.

Получение всех записей

Начнём с метода получения всех стран. Его реализация довольно проста: мы просто вызываем целевой метод у нашего dao,

@Service
public class CountryServiceImpl implements CountryService {

    @Autowired
    private CountryDao countryDao;

    @Override
    public List<Country> getAll() {
        return countryDao.findAllByOrderByIdDesc();
    }
}

Обработчик входящих http-запросов (контроллер) будет выглядеть следующим образом:

@RestController
@RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE)
public class GeoController {

    @Autowired
    private CountryService countryService;

    @GetMapping("/countries")
    public List<Country> getAllCountries() {
        return countryService.getAll();
    }
}

@RequestMapping задаёт базовый адрес, по которому можно обращаться к целевым методам, а параметр produces указывает, что ответ мы всегда будем возвращать в формате json. Далее мы внедряем наш сервисный слой через @Autowired. Аннотация @GetMapping указывает, что данный метод обрабатывает GET-запрос, а также задаёт дополнительный сегмент адреса.

Параметры подключения к БД нужно вынести в отдельный файл и либо положить его в папку resources нашего проекта, либо указывать при запуске в командной строке через параметр --spring.config.location. Spring поддерживает несколько форматов настроек, но я предлагаю использовать yaml:

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/example_db
    username: example_user
    password: "secret password"
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQL9Dialect
        show_sql: true

В настройках в секции datasource мы должны указать драйвер для работы с целевой СУБД, полный путь до БД, логин и пароль. В секции jpa мы указываем соответствующий диалект, который учитывает все особенности нашей СУБД, а также включаем вывод генерируемых sql-запросов в консоль при помощи параметра show_sql.

Теперь если мы выполним get-запрос по адресу http://127.0.0.1:8080/api/countries/, то получим список всех записей, которые есть в таблице country.

Поиск по id и по имени

Добавим в CountryServiceImpl ещё два метода:

@Override
public Country getById(int id) {
    Country country = countryDao.findOne(id);
    if (country == null) {
        throw new NotFoundException(id);
    }
    return country;
}

@Override
public Country getByName(String name) {
    Country country = countryDao.findByName(name);
    if (country == null) {
        throw new NotFoundException(name);
    }
    return country;
}

В методе getByName() для поиска по имени мы вызываем наш одноимённый метод из dao-слоя. В методе getById() для поиска по номеру записи вызываем стандартный метод findOne(), который ищет запись по указанному id, а если такой записи нет, то возвращает null. В случае, если запись не найдена, мы кидаем исключение NotFoundException.

@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {

    public NotFoundException(int id) {
        super("Country with id=" + id + " not found");
    }

    public NotFoundException(String name) {
        super("Country with name=" + name + " not found");
    }
}

Реализация этого исключения довольно проста. Аннотация @ResponseStatus позволяет указать, какой http-статус вернуть в случае ошибки. Согласно rest-архитектуре, если запись не найдена, следует возвращать код 404.

Добавим новые обработчики в контроллер:

@GetMapping("/countries/{countryId:\\d+}")
public Country getCountryById(@PathVariable("countryId") int id) {
    return countryService.getById(id);
}

@GetMapping("/countries/{countryName:\\D+}")
public Country getCountryById(@PathVariable("countryName") String name) {
    return countryService.getByName(name);
}

Поскольку оба запроса принимают по одному параметру, разделим их по типу через регулярное выражение. id записи у нас всегда числовое, о чём говорит регулярка \d+. А имя у нас всегда «не число», о чём говорит выражение \D+. @PathVariable позволяет получать именованную часть урла и подставлять её как параметр метода.

Теперь мы можем выполнять поиск страны, добавляя к урлу http://127.0.0.1:8080/api/countries/ номер искомой страны или её имя. Если же запись не найдена, получим соответствующий ответ в формате json и http-статус 404.

Добавление и удаление записи

В rest-архитектуре создание записи производится через POST-запрос, а обновление - через PUT. При этом параметры запроса можно передавать в теле этого запроса. В нашем случае единственным параметром запроса на создание новой страны будет её имя. Для этого создадим класс, в который будет мапится входящий json.

public class CountryRequest {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Тогда запросы на добавление и обновление страны в нашем сервисе будут выглядеть так:

@Override
public Country create(CountryRequest request) {
    Country country = new Country();
    country.setName(request.getName());
    return countryDao.save(country);
}

@Override
public Country update(int id, CountryRequest request) {
    if (!countryDao.exists(id)) {
        throw new NotFoundException(id);
    }
    Country country = new Country();
    country.setId(id);
    country.setName(request.getName());
    return countryDao.save(country);
}

CrudRepository для добавления и обновления предоставляет один метод - save(). Однако при обновлении мы получаем id целевой записи, поэтому должны предварительно проверить, что такая запись действительно есть. Для этого используем стандартный метод exists(). Если запись не найдена - кидаем исключение. Иначе создаём новый объект Country и инициализируем его соответствующими значениями.

В контроллер добавим следующие методы:

@PostMapping("/countries")
@ResponseStatus(HttpStatus.CREATED)
public Country createCountry(@RequestBody CountryRequest request) {
    return countryService.create(request);
}

@PutMapping("/countries/{countryId}")
public Country updateCountry(@PathVariable("countryId") int id, @RequestBody CountryRequest request) {
    return countryService.update(id, request);
}

@PostMapping и @PutMapping отвечают за связь с POST- и PUT-запросами соответственно, а @RequestBody связывает тело входящего запроса с нужным классом.

Теперь мы можем добавлять новые страны следующим образом:

curl -X POST -H 'Content-Type: application/json' -i 'http://127.0.0.1:8080/api/countries/' --data '{
"name":"Россия"
}'

В ответ мы получим сущность с id, которое было присвоено ей при вставке в таблицу.

Удаление записи

Удаление записи производится через стандартный метод delete(), но перед этим мы проверяем существование записи с таким id и если не находим, то кидаем исключение:

@Override
public void delete(int id) {
    if (!countryDao.exists(id)) {
        throw new NotFoundException(id);
    }
    countryDao.delete(id);
}

В контроллер добавим такой метод:

@DeleteMapping("/countries/{countryId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteCountry(@PathVariable("countryId") int id) {
    countryService.delete(id);
}

@DeleteMapping указывает на обработчик DELETE-запросов, а @ResponseStatus говорит, что в случае успеха вернём 204 http-статус - «No content».

Теперь мы можем удалять записи из таблицы, если знаем id.

Итоги

Как видите, при помощи абстракции CrudRepository мы получили всю базовую функциональность для работы с сущностью в БД и при этом не написали ни одного sql-запроса.


Облако тэгов

Kotlin, Java, Java 16, Java 11, Java 10, Java 9, Java 8, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.

Последние статьи


Комментарии

Добавить комментарий

×

devmark.ru