1. Вартість з’єднань із БД
DataSource уже знає, куди підключатися. Далі постають три практичні запитання: як застосунок отримуватиме з’єднання в реальній роботі, як нам бачити справжній SQL і як не дозволити Hibernate змінювати схему без нашого відома.
Якщо дивитися на базу як на «функцію, яку можна викликати», легко вирішити, що все просто: потрібні дані — відкрили з’єднання, прочитали їх, закрили. На практиці з’єднання з PostgreSQL — це реальний мережевий і серверний ресурс, який не створюється безкоштовно. Є рукостискання, авторизація, виділення ресурсів на боці сервера, підтримка стану й багато чого ще. І саме тут у бекенд-розробника починається перше інфраструктурне дорослішання.
Що відбувається під час нового з’єднання
Коли ваш код робить dataSource.getConnection(), це не «чарівне посилання на таблицю». Найчастіше це створення або видача вже готового TCP-з’єднання до сервера БД, а далі — логічний контекст сесії, усередині якого PostgreSQL обслуговує ваші запити.
Навіть у локальній розробці це помітно: якщо на кожен запит створювати нове з’єднання, застосунок почне гальмувати. А якщо додати реальне навантаження — кількох користувачів або фонові задачі, — ви дуже швидко вперетесь в обмеження за кількістю з’єднань на боці бази. PostgreSQL не зобов’язаний радіти тому, що ви щосекунди заводите нові «знайомства», як надто товариська людина на вечірці.
Щоб відчути ідею, уявіть, що кожен SQL-запит — це дзвінок у кол-центр. Якщо для кожного запитання ви щоразу набираєте номер, проходите робота, називаєте паспортні дані й чекаєте оператора, ви не просто витрачаєте час — ви створюєте черги та зайве навантаження на систему. Набагато розумніше «тримати лінію» й повторно використовувати з’єднання.
Ризики з’єднання на кожен запит
У навчального проєкту є підступна пастка: він спочатку маленький, і здається, що будь-які рішення «й так зійдуть». Але звички, які ви закріпите зараз, потім переїдуть у комерційний код. А створювати з’єднання «на кожен запит» — це звичка з розряду «а навіщо взагалі гальма в машині, якщо я поки що їжджу лише двором».
Навіть якщо продуктивність сьогодні вас не хвилює, з’єднання важливі ще й тому, що база обмежує їх кількість. Якщо не повторно використовувати з’єднання, можна отримати ситуацію, коли застосунок наче працює, але під невеликим навантаженням раптово починає падати з помилками «не можу отримати з’єднання». Для новачка це особливо неприємно: здається, що «SQL-то простий», а ламається все на рівні інфраструктури.
2. Пул з’єднань: ідея та модель
Щоб не створювати з’єднання щоразу з нуля, індустрія давно вигадала пул з’єднань (connection pool). Пул — це як невелика парковка готових машин: ви не купуєте автомобіль на кожну поїздку, а берете один із уже заведених, катаєтеся, а потім повертаєте його назад. І ось тут важливий момент: у коді ви «закриваєте» з’єднання, але фізично найчастіше не знищуєте його — ви повертаєте його в пул.
DataSource і пул: ролі
У Spring Boot ви найчастіше взаємодієте з базою через DataSource. І важливо пам’ятати, що DataSource — це не «один конкретний connection». Це об’єкт, який уміє видавати з’єднання. У сучасному застосунку майже завжди DataSource усередині — це і є пул з’єднань або його обгортка.
Схематично це виглядає так:
flowchart LR
App[Ваш код у Spring Boot] --> DS[DataSource]
DS --> Pool[пул HikariCP]
Pool --> PG[(PostgreSQL)]
Тобто ваш код не думає: «як мені створити TCP-з’єднання?». Він думає: «дай мені з’єднання». А далі DataSource уже вирішує, створити нове, якщо це допустимо, або видати готове з пулу.
Закриття з’єднання в пулі
Найчастіша помилка початківців — відкрити з’єднання й забути його закрити. Але навіть якщо ви закриваєте, важливо розуміти семантику: за наявності пулу connection.close() найчастіше означає «я завершив роботу, можеш повернути це з’єднання в пул». Це не обов’язково означає «обірвати TCP і все знищити».
Тому в коді важливо зберігати дисципліну: з’єднання потрібно отримувати якомога пізніше й закривати якомога раніше. Класична форма — try-with-resources. Виглядає буденно, зате рятує від витоків з’єднань так само надійно, як ремінь безпеки рятує від раптової зустрічі з реальністю.
Мініприклад (це не «як ми писатимемо бізнес-код», а просто демонстрація дисципліни):
import javax.sql.DataSource;
public class ConnectionDemo {
public static void demo(DataSource dataSource) throws Exception {
// Важливо: try-with-resources гарантує виклик close() навіть у разі помилки.
// У разі пулу close() зазвичай означає "повернути з’єднання в пул", а не "обірвати TCP".
try (var connection = dataSource.getConnection()) {
// Найпростіша перевірка: з’єднання живе й відповідає.
System.out.println(connection.isValid(2)); // true
}
// Після виходу з блоку з’єднання коректно звільнено (повернено в пул).
}
}
Так, тут немає «корисної логіки». Зате є корисна звичка: взяв ресурс — повернув ресурс.
3. HikariCP як пул за замовчуванням
Коли ви підключаєте spring-boot-starter-data-jpa, у вас з’являється не тільки JPA/Hibernate, а й уся базова JDBC-інфраструктура. А разом із нею постає й запитання: який пул з’єднань використовувати. У сучасному Spring Boot за замовчуванням ви найчастіше зустрінете HikariCP. Це швидкий, стабільний і давно вже де-факто стандартний пул, а не якась екзотика.
Звідки береться HikariCP
Для новачка це виглядає так: «я нічого спеціально не підключав, а пул уже є». І так, майже так і відбувається: Spring Boot підтягне потрібні залежності через стартери, а HikariCP стане пулом за замовчуванням, якщо він є в classpath — а він там зазвичай є.
Це не магія «з повітря». Це просто зручний базовий вибір платформи: замість того щоб кожному студенту розповідати про три конкуруючі пули й десяток параметрів, ми беремо один зрілий варіант і будуємо навколо нього зрозумілу основу. Саме так і працює Boot: він дає розумні значення за замовчуванням, щоб ви не витрачали тиждень на вибір велосипеда для доставки піци.
Перевірити, що ваш DataSource — це Hikari, можна одноразовою діагностикою:
import javax.sql.DataSource;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TemporaryDataSourceDebugConfig {
@Bean
CommandLineRunner showDataSourceOnce(DataSource dataSource) {
// Одноразовий діагностичний код: побачити реальний клас DataSource і прибрати.
return args -> {
System.out.println(dataSource.getClass().getName());
// com.zaxxer.hikari.HikariDataSource (найчастіше)
};
}
}
Побачили реальний клас DataSource, переконалися, що це Hikari, і прибрали діагностичний код. Для постійного життя проєкту такий runner не потрібен: він корисний лише як коротка перевірка.
Мінімальні параметри пулу
У HikariCP, як і в будь-якого пулу, багато налаштувань. Але на старті курсу достатньо зрозуміти лише кілька, щоб перестати боятися «страшного YAML». Головне — не намагатися тюнити все підряд. Пул — це не домашній кіт: він не стане кращим від того, що ви чіпаєте його кожні п’ять хвилин.
Ось базова таблиця, яку корисно тримати в голові:
| Параметр | Що означає по-людськи | Що станеться, якщо виставити навмання |
|---|---|---|
| maximum-pool-size | Максимум з’єднань, які застосунок може тримати одночасно | Занадто багато — навантажите БД. Занадто мало — будуть черги й тайм-аути |
| minimum-idle | Скільки з’єднань пул намагається тримати готовими «на старті» | Занадто багато — даремно тримаєте ресурси. Занадто мало — перші запити можуть бути повільнішими |
| connection-timeout | Скільки чекати на вільне з’єднання, якщо пул зайнятий | Мале значення дасть швидкі помилки. Величезне — застосунок «зависає і мовчить» |
| max-lifetime | Скільки живе з’єднання до «планової заміни» | Якщо занадто мало — доведеться часто пересоздавати з’єднання |
| idle-timeout | Скільки з’єднання може простоювати, перш ніж пул його відпустить | Неправильні значення можуть призводити до зайвих перепідключень |
На цьому рівні ми не намагаємося стати DBA. Ми просто вчимося сприймати з’єднання як ресурс і розуміти кілька «ручок», якими цей ресурс обмежується.
Мінімальна конфігурація в application-dev.yml
Тепер зафіксуймо налаштування, які розумні для навчального сервісу. Ми хочемо, щоб з’єднань було небагато, але достатньо, і щоб усе це явно було видно в конфігурації.
Файл: src/main/resources/application-dev.yml
spring:
datasource:
hikari:
# Верхня межа: скільки одночасних з’єднань застосунок може тримати.
maximum-pool-size: 10
# Скільки з’єднань тримати "теплими" (готовими) навіть під час простою.
minimum-idle: 2
# Скільки (у мс) чекати на вільне з’єднання, перш ніж завершитися з помилкою.
connection-timeout: 2000
Тут важливі два моменти. По-перше, значення малі й зрозумілі. По-друге, це лежить у dev-профілі, тобто ви свідомо кажете: «у розробці мені потрібна саме така інфраструктура». У реальному продукті значення можуть бути іншими, але думка лишається: пул — це частина інфраструктури, і вона має жити в конфігурації, а не в голові.
4. SQL-логи і видимість запитів
Spring Data JPA і Hibernate дуже легко сприймати як магію: ви «викликали метод», а десь там усе саме пішло в базу. Щоб магія не стала релігією, нам потрібна SQL-видимість. Тобто здатність бачити запити, які реально йдуть у БД, хоча б у dev-режимі. Це основа всього курсу, тому що далі ми багато разів порівнюватимемо «як я думав» і «що сталося насправді».
show-sql і логери: різний характер
На старті виникне спокуса просто ввімкнути spring.jpa.show-sql=true і зрадіти. Це працює, але є нюанс: show-sql друкує SQL доволі «в лоб», часто прямо в stdout, і не завжди добре поєднується з нормальною системою логування застосунку. Спосіб простий, але галасливий і не надто керований.
Більш дорослий шлях — використовувати звичайні логери через logging.level... і керувати рівнем логування, форматуванням і профілями. На старті курсу ми можемо використовувати обидва підходи: show-sql — для швидкого розуміння, а логери — як основний інструмент, коли почнеться серйозна діагностика.
Головне — пам’ятати: SQL-логи — це інструмент навчання і діагностики, а не постійний режим «хай у консоль завжди летить усе, що рухається». Якщо залишити SQL-видимість увімкненою назавжди, за два дні ви почнете сприймати її як фон. Це як пожежна сигналізація, яка верещить 24/7: формально працює, по суті — марна.
Налаштування SQL-видимості в application-dev.yml
Зробімо мінімальний і зрозумілий набір налаштувань, який дасть читабельний SQL.
Файл: src/main/resources/application-dev.yml
spring:
jpa:
# Швидкий спосіб побачити SQL у dev-режимі (часто друкується "в лоб").
show-sql: true
properties:
hibernate:
# Робить SQL читабельнішим (перенесення, відступи).
format_sql: true
Цього достатньо, щоб побачити SQL і зробити його більш читабельним. Коли ви пізніше почнете порівнювати поведінку різних запитів, за format_sql ви ще подякуєте собі.
Якщо хочете більш дорослий варіант через логери, можна додати рівні логування:
Файл: src/main/resources/application-dev.yml
logging:
level:
# Показує текст SQL-запитів, які виконує Hibernate.
org.hibernate.SQL: DEBUG
# Показує значення параметрів (те, що підставилося замість ?).
org.hibernate.orm.jdbc.bind: TRACE
Перший логер показує SQL, другий — значення параметрів. Якщо ви колись бачили запит виду where id=? і думали «ну і що мені з цим робити», саме другий логер перетворює це запитання на «а, ось які значення реально підставилися». Так, це гучніше, тому все й живе в dev-профілі.
Тут є один важливий нюанс. Ці логери показують саме SQL, який генерує і надсилає Hibernate під час ORM-операцій. Перевірка dataSource.getConnection() або connection.isValid(2) працює на рівні JDBC, тому за org.hibernate.SQL у такий момент може бути тиша — і це нормально. Така перевірка відповідає на запитання «чи дотягуємося до бази?», а не «який SQL будує ORM?».
Як читати SQL-логи без паніки
Спочатку SQL-логи виглядають як «Матриця». Але читати їх можна за дуже простим алгоритмом: спочатку шукаєте тип операції — select, insert, update, delete, — потім таблицю, потім умови (where) і вже після цього дивитеся на кількість запитів.
На цьому етапі курсу нам не потрібно оптимізувати запити. Нам потрібно навчитися помічати очевидне: «я хотів одну операцію, а отримав п’ять» або «я думав, що схема не змінюється, а вона раптово створюється заново». Це той самий дебаг, тільки замість змінних у вас SQL.
5. ddl-auto: демо і контроль схеми
Коли ви починаєте працювати з JPA, Hibernate вміє автоматично створювати схему бази, спираючись на ваші класи сутностей. Для демо це виглядає чудово: написали кілька анотацій — і таблиці зʼявилися. Але саме тут зручність дуже швидко перетворюється на погану звичку. Тому до ddl-auto ми ставимося як до тимчасової підпірки, а не як до джерела істини про схему.
Schema generation і роль Hibernate
ddl-auto — це налаштування, яке каже Hibernate: «що робити зі схемою БД під час старту застосунку». Hibernate може спробувати створити таблиці, оновити їх, перевірити відповідність і так далі. Це стосується DDL (Data Definition Language): create table, alter table, drop table — тобто структури бази.
І ось ключова думка: SQL-логи показують, які запити йдуть, а ddl-auto впливає на те, чи змінюється схема. Ці речі легко переплутати, тому що і там, і там ви бачите SQL. Але сенс зовсім різний: SQL-логи — це «підсвічування того, що відбувається», а ddl-auto — «рука, яка реально рухає меблі в квартирі».
Важливий нюанс саме для поточного стану проєкту: поки в нас ще немає @Entity, Hibernate майже нічого не генерує. Але налаштування ми фіксуємо вже зараз, щоб завтра, коли з’являться перші сутності, нас не здивували раптові drop table або запитання «чому таблиця зникла».
Режими ddl-auto простими словами
Режимів кілька. І краще один раз побачити їх у таблиці, ніж десять разів перечитувати «а що означає update».
| ddl-auto | Що робить | Коли допустимо | Чому небезпечно як значення за замовчуванням |
|---|---|---|---|
| create | Видаляє стару схему і створює нову під час старту | Дуже короткі демо, де дані не шкода | Стирає дані, може несподівано «обнулити світ» |
| create-drop | Як create, але ще й намагається видалити схему під час зупинки застосунку | Навчальні сценарії, швидкі експерименти | Дані зникають (іноді «надто успішно») |
| update | Намагається «підправити» схему під entity-модель | Іноді в dev, якщо ви розумієте ризики | Непередбачувано, погано відтворювано, не годиться для командної розробки |
| validate | Нічого не змінює, але перевіряє відповідність schema ↔ відображення | Коли схемою керують окремо | Упаде за невідповідності (і це нормально) |
| none | Hibernate не чіпає схему | Коли ви не хочете сюрпризів | Вимагає, щоб схема вже була створена іншим способом |
Якщо запам’ятати лише одну думку, то вона така: create і create-drop — це «зручно, але все зітреться», update — це «ніби зручно, але потім буде боляче», а validate/none — це «дисципліна, зате без сюрпризів».
Безпечне використання ddl-auto
У межах курсу ми можемо використовувати create-drop як місток на ранніх етапах, коли нам важливо швидко побачити, що сутності починають жити в БД. Але тримати це потрібно під контролем: вмикати лише в dev-профілі й пам’ятати, що режим тимчасовий.
Найбезпечніший стиль для навчального старту виглядає так: у application-dev.yml вмикаємо демо-режим, а в загальному application.yml — або хоча б у себе в голові — тримаємо думку, що в майбутньому схемою потрібно буде керувати окремо. Hibernate не повинен тихо переписувати базу без дозволу.
Файл: src/main/resources/application-dev.yml
spring:
jpa:
hibernate:
# Демо-режим: на старті створює схему заново, під час зупинки видаляє.
# Використовувати лише там, де дані не шкода (наприклад, у dev).
ddl-auto: create-drop
А якщо ви хочете режим «нічого не чіпай, просто підключайся», то:
Файл: src/main/resources/application-dev.yml
spring:
jpa:
hibernate:
# Безпечний режим: Hibernate не намагається створювати/змінювати таблиці.
# Схема має бути підготовлена заздалегідь (міграціями, руками, чим завгодно — але не Hibernateʼом).
ddl-auto: none
І ще одна практична зв’язка: у нас база живе в Docker, і ми використовуємо volume, щоб дані зберігалися. Якщо ввімкнути create-drop, можна дуже здивуватися: «volume ж зберігає дані, чому все зникло?» Відповідь проста: volume зберігає те, що всередині, але якщо застосунок сам видалив таблиці, зберігати вже нічого. Volume не чарівник, він просто чесний.
6. Типові помилки під час роботи з БД
Помилка №1: «Підкручу maximum-pool-size до 200 — і все буде швидко».
Пул з’єднань не прискорює застосунок магічно. Він обмежує й стабілізує доступ до БД. Якщо поставити величезний пул, ви не отримаєте «прискорення в 20 разів», а просто збільшите шанс завалити БД з’єднаннями і зробити гірше всім, включно із собою. На старті краще малі, осмислені значення й розуміння, навіщо вони потрібні.
Помилка №2: забувати закривати з’єднання й потім звинувачувати PostgreSQL, Spring і «всесвіт».
Витік з’єднань — одна з найнеприємніших інфраструктурних поломок: застосунок може стартувати й навіть якийсь час працювати, а потім раптово «почати зависати». Для новачка це виглядає як містика, але причина часто дуже проста: з’єднання не повернулися в пул. try-with-resources — не прикраса, а страховка.
Помилка №3: тримати SQL-логи увімкненими завжди, у всіх профілях і на максимальному рівні шуму.
SQL-логи корисні, поки ви їх читаєте. Якщо вони перетворюються на постійний водоспад тексту, мозок перестає їх помічати. Набагато корисніше вмикати SQL-видимість у dev-профілі й підвищувати деталізацію лише тоді, коли ви справді розслідуєте поведінку запиту.
Помилка №4: плутати «показувати SQL» і «керувати схемою».
show-sql і логери допомагають побачити, що відбувається. ddl-auto змінює або перевіряє структуру БД. Якщо змішати це в голові, дуже легко зробити небезпечну річ «за звичкою»: наприклад, увімкнути create-drop, думаючи, що це просто «для логів». А потім виявити, що таблиці зникли разом із даними.
Помилка №5: використовувати ddl-auto=update як вічну підпірку.
update часто здається золотою серединою: «і дані не стирає, і таблиці якось підлаштовує». На практиці це один із найпідступніших режимів: він не дає відтворюваності, може поводитися по-різному на різних середовищах і легко перетворює схему на «те, що вийшло». У навчальному проєкті його можна коротко згадати, але як стиль життя — погана ідея.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ