Spring Data JDBC: JdbcTemplate и NamedParameterJdbcTemplate

В этой статье рассмотрим, как с помощью JdbcTemplate обращаться к базе и выполнять SQL-операторы. В следующей статье речь пойдет о более продвинутом CrudRepository и других возможностях Spring Data JDBC.

Maven-зависимость

Чтобы включить в Spring Boot проект Spring Data JDBC, добавим в POM-файл:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

Мы будем использовать базу H2, поэтому добавим также:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Но сначала рассмотрим пример подключения к базе и выполнения на ней select из обычного не Spring приложения. Это поможет понять, что JdbcTemplate делает под капотом.

Без JdbcTemplate

Допустим у нас есть сущность Animal:

public class Animal {

    private long id;
    private String name;
    // getters/setters/constructors
}

В базе она хранится в таблице animal. Чтобы соединиться с базой и выбрать animal c id=1 в простом приложении без Spring, необходимо выполнить кучу действий:

public class JdbcMain {

    public static void main(String[] args) {
        Animal animal = findById(1l);
    }

    public static Animal findById(long id) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DriverManager.getConnection(
                    "jdbc:postgresql://localhost:5432/dbname", "postgres", "password");
            statement = connection.prepareStatement(
                    "select id, name from animal where id=?");
            statement.setLong(1, id);
            resultSet = statement.executeQuery();
            Animal animal = null;
            if (resultSet.next()) {
                animal = new Animal(
                        resultSet.getInt("id"),
                        resultSet.getString("name"));
            }
            return animal;
        } catch (SQLException e) {

        } finally {
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                }
            }

            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                }
            }
        }
        return null;
    }
}

В коде выше:

  • Открывается соединение Connection.
  • Создается PreparedStatement.
  • Выполняется запрос.
  • Идет итерация по ResultSet.
  • затем ResultSet, PreparedStatement и Connection закрываются в блоке finally, причем эти действия тоже могут выбросить исключение.

Собственно полезного кода — пара строк, остальное — подготовка (boilerlate).

JdbcTemplate берет всю дополнительную работу на себя, на нас остается уникальный код — конкретный запрос.

C JdbcTemplate (точнее, лучше NamedParameterJdbcTemplate)

Мы будем использовать NamedParameterJdbcTemplate, который отличается от JdbcTemplate тем, что параметры можно задавать именем, а не знаком «?«.

Рассмотрим тот же пример выборки Animal по id c NamedParameterJdbcTemplate:

@Repository
public class AnimalRepository {

    private final NamedParameterJdbcTemplate jdbc;

    @Transactional
    public Animal getById(long id) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        return jdbc.queryForObject("select * from animal where id=:id", params, new AnimalMapper());

    }

    //...
}

где AnimalMapper:

public class AnimalMapper implements RowMapper<Animal> {
    @Override
    public Animal mapRow(ResultSet resultSet, int i) throws SQLException {
        final int id = resultSet.getInt("id");
        final String name = resultSet.getString("name");
        return new Animal(id, name);
    }
}

Как видите, так все выглядит гораздо проще.

  • Мы аннотировали репозиторий с помощью @Repository — так он стал бином.
  • Внедрили NamedParameterJdbcTemplate.
  • И выполнили запрос.
  • Параметром передали AnimalMapper, который преобразует ResultSet в Animal.

Настройка соединения

Для соединения с базой Spring Boot создает бин dataSource.

Параметры доступа к базе прописываем в application.yml:

spring:
  h2:
    console:
      enabled: true
      path: /h2-console
  datasource:
     url: jdbc:h2:mem:testdb
     username: sa
     password:

Кроме того, мы включили h2-console, чтобы по адресу

localhost:8080/h2-console

можно было соединиться с базой после запуска приложения.

Создание и заполнение базы

Чтобы база создавалась и наполнялась, просто поместим в ресурсы файлы

schema.sql
data.sql

с SQL-операторами, создающими таблицы и добавляющими данные соответственно.

Папка ресурсов
Папка ресурсов

Поскольку мы используем  in-memory базу, данные в ней пропадают и создаются заново при перезапуске приложения.

Методы добавления, редактирования, выбора всех строк

И рассмотрим еще несколько методов.

@Transactional
public Animal insert(Animal animal) {
    Map<String, Object> params = new HashMap<>();
    params.put("name", animal.getName());
    SqlParameterSource paramSource = new MapSqlParameterSource(params);

    GeneratedKeyHolder holder = new GeneratedKeyHolder();

    jdbc.update("insert into animal (name) values (:name)", paramSource, holder);
    animal.setId(holder.getKey().longValue());

    return animal;
}

@Transactional
public int update(String name, long id) {
    Map<String, Object> params = new HashMap<>();
    params.put("name", name);
    params.put("id", id);
    int n= jdbc.update("update animal set name=:name where id=:id", params);
    return n ;
}


@Transactional
public List<Animal> getAll() {
    
    return jdbc.query("select * from animal", new AnimalMapper());
}

Как видите, запросы, модифицирующие базу, делаются в методе jdbc.update().

При добавлении строки с помощью GeneratedKeyHolder мы получаем автоматически генерируемый в базе первичный ключ.

@Transactional

Все методы стоит аннотировать @Transactional, чтобы они выполнялись в рамках одной транзакции (на случай добавления других запросов в метод).

Итоги

Исходный код примера доступен на GitHub.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *