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 ▸
- Подключение:
DriverManager.getConnection(url, user, password)→Connection - Запрос:
connection.prepareStatement("SELECT * FROM tasks WHERE id = ?")→PreparedStatement - Параметры:
stmt.setLong(1, 42) - Выполнение:
stmt.executeQuery()(для SELECT) илиstmt.executeUpdate()(для INSERT/UPDATE/DELETE) →ResultSet - Чтение:
rs.next()→rs.getString("title") - Закрытие: в обратном порядке (
ResultSet→Statement→Connection)
⚠️ Почему 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Зависимости
build.gradle.ktsdependencies { 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
application-dev.ymlspring: 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 -
3SQL-схема (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
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 RowMapperTASK_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Запуск с 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---
Зачем это на собесе
После урока ты должен уметь ответить на:
5 вопросов на углубление
Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.
Разбор внутри: RETURN_GENERATED_KEYS, KeyHolder и почему id иначе будет null.
Разбор внутри: очередь ожидания, connectionTimeout и правильная ошибка вместо вечного зависания.
Разбор внутри: меньше boilerplate, автоматическое закрытие ресурсов и RowMapper.
Разбор внутри: maxLifetime, keepaliveTime и проверка соединений.
Разбор внутри: какие logger-и включать и что смотреть в логах.
Готов идти дальше?
Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.
🚀 Перейти к Уроку 6Сначала пройди все секции и выбери хотя бы 1 вопрос
⬅️ Назад к Уроку 4