УРОК 5 / 30 0%

PostgreSQL + JDBC + HikariCP

🎯

Цель урока

Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.

🧠

Теория · для собеса

1 Что такое JDBC

JDBC (Java Database Connectivity) — это Java API для общения с реляционными БД через SQL.


Java app → JDBC API → JDBC Driver (postgres-jdbc.jar) → PostgreSQL
2 Шаги JDBC
  1. Подключение: DriverManager.getConnection(url, user, password)Connection
  2. Запрос: connection.prepareStatement("SELECT * FROM tasks WHERE id = ?")PreparedStatement
  3. Параметры: stmt.setLong(1, 42)
  4. Выполнение: stmt.executeQuery() (для SELECT) или stmt.executeUpdate() (для INSERT/UPDATE/DELETE) → ResultSet
  5. Чтение: rs.next()rs.getString("title")
  6. Закрытие: в обратном порядке (ResultSetStatementConnection)

⚠️ Почему PreparedStatement, а не Statement?

  • Защита от SQL injection: параметры передаются отдельно от SQL
  • Кеширование: СУБД кеширует план запроса
  • Типобезопасность: setLong, setString с проверкой
3 Пул соединений (Connection Pool)

Создавать Connection дорого (TCP-хендшейк, TLS, авторизация). Решение — пул: держим N готовых соединений, выдаём по запросу, возвращаем после использования.

HikariCP — самый быстрый и популярный пул для JVM.


TaskService → DataSource (HikariCP) → Connection #3 (из пула в 10)
                                          ↓
                                       PostgreSQL
4 Spring и DataSource

Spring Boot автоматически настраивает HikariCP, если в classpath есть:

  • spring-boot-starter-jdbc ИЛИ spring-boot-starter-data-jpa
  • JDBC-драйвер (org.postgresql:postgresql)

Всё сводится к настройке spring.datasource.* в yaml.

---

💻

Практика: улучшаем CRUD

  1. 1
    Зависимости build.gradle.kts
    
    dependencies {
        implementation("org.springframework.boot:spring-boot-starter-web")
        implementation("org.springframework.boot:spring-boot-starter-jdbc")
        runtimeOnly("org.postgresql:postgresql")
        implementation("org.flywaydb:flyway-core")
        implementation("org.flywaydb:flyway-database-postgresql")
    }
    
  2. 2
    application-dev.yml
    
    spring:
      datasource:
        url: jdbc:postgresql://localhost:5432/taskflow_dev
        username: taskflow
        password: dev_password
        driver-class-name: org.postgresql.Driver
        hikari:
          maximum-pool-size: 10
          minimum-idle: 2
          connection-timeout: 20000  # 20 сек
          idle-timeout: 300000       # 5 мин
          max-lifetime: 1200000      # 20 мин (должно быть меньше БД-таймаута)
          pool-name: TaskFlowHikariPool
    
  3. 3
    SQL-схема (Flyway миграция)

    src/main/resources/db/migration/V1__init_tasks.sql:

    
    CREATE TABLE tasks (
        id BIGSERIAL PRIMARY KEY,
        title VARCHAR(200) NOT NULL,
        description TEXT,
        done BOOLEAN NOT NULL DEFAULT FALSE,
        created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
    );
    
    CREATE INDEX idx_tasks_done ON tasks(done);
    CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);
    
  4. 4
    repository/TaskRepository.java (JDBC-версия)
    
    package com.taskflow.repository;
    
    import com.taskflow.model.Task;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.jdbc.support.GeneratedKeyHolder;
    import org.springframework.jdbc.support.KeyHolder;
    import org.springframework.stereotype.Repository;
    
    import java.sql.PreparedStatement;
    import java.sql.Statement;
    import java.util.List;
    import java.util.Optional;
    
    @Repository
    public class TaskRepository {
        private final JdbcTemplate jdbc;
    
        public TaskRepository(JdbcTemplate jdbc) {
            this.jdbc = jdbc;
        }
    
        private static final RowMapper TASK_ROW_MAPPER = (rs, rowNum) -> new Task(
            rs.getLong("id"),
            rs.getString("title"),
            rs.getString("description"),
            rs.getBoolean("done")
        );
    
        public List findAll() {
            return jdbc.query("SELECT * FROM tasks ORDER BY created_at DESC", TASK_ROW_MAPPER);
        }
    
        public Optional findById(Long id) {
            return jdbc.query(
                "SELECT * FROM tasks WHERE id = ?",
                TASK_ROW_MAPPER,
                id
            ).stream().findFirst();
        }
    
        public Task create(Task task) {
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbc.update(connection -> {
                PreparedStatement ps = connection.prepareStatement(
                    "INSERT INTO tasks (title, description, done) VALUES (?, ?, ?)",
                    Statement.RETURN_GENERATED_KEYS
                );
                ps.setString(1, task.getTitle());
                ps.setString(2, task.getDescription());
                ps.setBoolean(3, task.isDone());
                return ps;
            }, keyHolder);
            Long newId = (Long) keyHolder.getKeys().get("id");
            return findById(newId).orElseThrow();
        }
    
        public Task update(Long id, Task task) {
            jdbc.update(
                "UPDATE tasks SET title = ?, description = ?, done = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
                task.getTitle(), task.getDescription(), task.isDone(), id
            );
            return findById(id).orElseThrow();
        }
    
        public int deleteById(Long id) {
            return jdbc.update("DELETE FROM tasks WHERE id = ?", id);
        }
    }
    
  5. 5
    Запуск с PostgreSQL
    
    # Через Docker (рекомендую)
    docker run -d \
      --name taskflow-postgres \
      -e POSTGRES_DB=taskflow_dev \
      -e POSTGRES_USER=taskflow \
      -e POSTGRES_PASSWORD=dev_password \
      -p 5432:5432 \
      postgres:16-alpine
    
    # Проверка
    docker ps
    docker logs taskflow-postgres
    
    
    # Запуск приложения — Flyway сам применит V1__init_tasks.sql
    ./gradlew bootRun
    

    В логах увидишь:

    
    Successfully applied 1 migration to schema: "public", version: V1
    HikariPool-1 - Starting...
    HikariPool-1 - Start completed.
    Started TaskFlowApplication in 3.45 seconds
    

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

Понимание JDBC = понимание JPA. JPA — это ORM поверх JDBC. Когда JPA «тормозит», ты спускаешься в SQL напрямую.
HikariCP — стандарт в Spring Boot. Умеет auto-reconnect, leak detection, мониторинг через JMX/Micrometer.
Flyway — миграции версионируются и применяются при старте. Невозможно «забыть» SQL-изменение.
Connection timeout = 20 сек — если пул исчерпан (например, медленные запросы), новые запросы падают быстро, а не висят вечно.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
Как получить сгенерированный id после INSERT в JDBC?

Разбор внутри: RETURN_GENERATED_KEYS, KeyHolder и почему id иначе будет null.

2
Что будет, если в HikariCP закончились соединения?

Разбор внутри: очередь ожидания, connectionTimeout и правильная ошибка вместо вечного зависания.

3
Чем JdbcTemplate лучше сырого DriverManager?

Разбор внутри: меньше boilerplate, автоматическое закрытие ресурсов и RowMapper.

4
Почему пул может вернуть мёртвое соединение и как это чинить?

Разбор внутри: maxLifetime, keepaliveTime и проверка соединений.

5
Как логировать HikariCP и SQL, чтобы понять проблему?

Разбор внутри: какие logger-и включать и что смотреть в логах.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 6

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 4
🎉
Новый тир
Новый тир достигнут!