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

RowMapper и ResultSetExtractor в Spring Boot

Исходники

3 августа 2021

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

Содержание

  1. RowMapper
  2. ResultSetExtractor
  3. Выводы

Spring Boot предоставляет два интерфейса для обработки выборки из БД: RowMapper и ResultSetExtractor. Давайте разберём их назначение, а также выясним, чем они различаются на примере справочника городов и стран.

RowMapper

Чаще всего при работе со списками в restful-сервисах, построенных на Spring Boot, вы будете использовать RowMapper. Этот класс обрабатывает отдельно каждую запись, полученную из БД, и возвращает уже готовый объект - модель данных. В большинстве случаев его вполне хватает.

Создадим простенький rest-контроллер, который будет возвращать список всех стран, которые заведены у нас в БД. Определение таблицы в СУБД postgres выглядит следующим образом:

CREATE TABLE public.country
(
  id serial,
  name character varying(50) NOT NULL,
  CONSTRAINT country_pk PRIMARY KEY (id)
)

Здесь тип serial представляет собой обычный integer, который автоматически увеличивается на 1 при добавлении каждой новой записи. То есть нет нужды при вставке явно указывать id.

Добавим туда несколько стран для примера:

insert into country (name) values ('Германия'); -- id = 1
insert into country (name) values ('Франция');  -- id = 2
insert into country (name) values ('Италия');   -- id = 3

Наша модель данных Country по типу и набору полей должна соответствовать структуре таблицы:

public class Country {

    private int id;
    private String name;

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

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

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

    @Autowired
    private GeoDao geoDao;

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

Здесь мы из контроллера сразу используем уровень работы с БД (GeoDao). В реальных приложениях между ними должен быть ещё сервисный слой, но для краткости я его пропущу, т.к. он довольно тривиален.

Слой работы с БД (dao):

@Repository
public class GeoDao {

    private static final String SQL_GET_ALL_COUNTRIES = "select * from country order by name";

    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;

    @Autowired
    private CountryMapper countryMapper;

    public List<Country> getAllCountries() {
        return jdbcTemplate.query(SQL_GET_ALL_COUNTRIES, countryMapper);
    }
}

Код маппера:

@Component
public class CountryMapper implements RowMapper<Country> {

    @Override
    public Country mapRow(ResultSet rs, int i) throws SQLException {
        Country country = new Country();
        country.setId(rs.getInt("id"));
        country.setName(rs.getString("name"));
        return country;
    }
}

Здесь мы реализуем интерфейс RowMapper, типизированный нашей моделью Country. Каждую строку здесь обрабатываем отдельно при помощи ResultSet. В качестве результата возвращаем уже готовый объект.

Теперь мы готовы к тому, чтобы запустить наше приложение и выполнить GET-запрос по адресу http://127.0.0.1:8080/geo/countries. В ответ получим такой json:

[{
  "id": 1,
  "name": "Германия"
}, {
  "id": 3,
  "name": "Италия"
}, {
  "id": 2,
  "name": "Франция"
}]

ResultSetExtractor

Теперь создадим вторую таблицу с городами, причём каждый город привязан к одной из стран. То есть это отношение «один-ко-многим». Структура таблицы будет выглядеть так:

CREATE TABLE public.city
(
  id serial,
  country_id integer NOT NULL,
  name character varying NOT NULL DEFAULT 50,
  CONSTRAINT city_pk PRIMARY KEY (id),
  CONSTRAINT city_country_fk FOREIGN KEY (country_id)
      REFERENCES public.country (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)

Связь с таблицей city происходит при помощи внешнего ключа country_id. Добавим в таблицу несколько записей:

insert into city (country_id, name) values (1, 'Берлин');
insert into city (country_id, name) values (2, 'Париж');
insert into city (country_id, name) values (3, 'Рим');
insert into city (country_id, name) values (1, 'Мюнхен');
insert into city (country_id, name) values (3, 'Венеция');

Предположим, что мы хотим возвращать города в таком виде, чтобы они были сгруппированы по странам. Сделать это на уровне БД можно при помощи join. Обратите внимание, что сортируем мы по названию страны.

select c.name as city_name, n.name as country_name from city c
left join country n on c.country_id = n.id order by n.name

Таким образом, в каждой строке у нас будет и название города, и название страны, в которой этот город находится.

Очевидно, что для группировки по странам нужно работать в контексте всей выборки одновременно, однако RowMapper работает только на уровне одной строки. И тут нам на помощь приходит ResultSetExtractor.

@Component
public class CityExtractor implements ResultSetExtractor<Map<String, List<String>>> {

    @Override
    public Map<String, List<String>> extractData(ResultSet rs)
            throws SQLException, DataAccessException {
        Map<String, List<String>> data = new LinkedHashMap<>();
        while (rs.next()) {
            String country = rs.getString("country_name");
            data.putIfAbsent(country, new ArrayList<>());
            String city = rs.getString("city_name");
            data.get(country).add(city);
        }
        return data;
    }
}

Типизируем его составным объектом Map. Это справочник, где ключ - строка, а значение - список строк (List). Важно, что в качестве реализации интерфейса Map мы выбрали не HashMap, а LinkedHashMap, который сохраняет порядок добавления ключей. Это нам нужно для того, чтобы страны шли в алфавитном порядке.

Далее в цикле обходим все строки, пока они есть, при помощи rs.next(), каждый раз переходя на новую строку. Сначала берём из текущей строки страну, которая будет ключом в нашем справочнике. Затем добавляем этот ключ с пустым списком городов, если такого ключа ещё нет, при помощи удобного метода putIfAbsent(). Затем извлекаем из текущей строки название города и добавляем его в список соответствующей страны.

Метод в dao будет выглядеть довольно просто (не забудьте добавить sql-запрос, указанный выше):

    @Autowired
    private CityExtractor cityExtractor;

    public Map<String, List<String>> getGroupedCities() {
        return jdbcTemplate.query(SQL_GET_ALL_CITIES, cityExtractor);
    }

В контроллере также не будет ничего сложного:

    @GetMapping("/cities")
    public Map<String, List<String>> getGroupedCities() {
        return geoDao.getGroupedCities();
    }

Теперь мы готовы выполнить GET-запрос на адрес http://127.0.0.1:8080/geo/cities и получить такой красивый json:

{
  "Германия": ["Берлин", "Мюнхен"],
  "Италия": ["Рим", "Венеция"],
  "Франция": ["Париж"]
}

Выводы

На данном примере наглядно видно, что если мы хотим одновременно извлекать данные из нескольких таблиц, то тут на помощь приходит ResultSetExtractor, который работает в контексте всей выборки целиком, позволяя группировать данные. А если мы работаем с одной таблицей, то тут вполне хватит и обычного RowMapper.


Облако тэгов

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