Даже если в проекте Spring Data и Hibernate, под ними JDBC. На собесе проверяют, понимаете ли вы, что происходит внизу: соединение, PreparedStatement, транзакция, пул.
Основные интерфейсы
DataSourceвыдаёт соединения (в проде это пул, например HikariCP).Connectionсессия с БД, на ней живёт транзакция.PreparedStatementскомпилированный запрос с параметрами?.ResultSetкурсор по строкам результата.
String sql = "SELECT id, name FROM users WHERE email = ?";
try (Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, email); // индексы с 1
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("name");
}
}
}
executeQuery для SELECT, executeUpdate для INSERT/UPDATE/DELETE (возвращает число строк), execute для чего угодно. Все ресурсы закрываются через try-with-resources: незакрытое соединение не вернётся в пул.
Statement и PreparedStatement
Statement с конкатенацией строк открывает дорогу SQL-инъекции:
// плохо: email = "' OR '1'='1"
stmt.executeQuery("SELECT * FROM users WHERE email = '" + email + "'");
PreparedStatement передаёт параметры отдельно от текста запроса, база никогда не интерпретирует их как SQL. Плюс план запроса может переиспользоваться. Параметром нельзя подставить имя таблицы или колонки и направление сортировки: их проверяют по белому списку.
Транзакции
По умолчанию соединение в режиме autoCommit = true: каждый запрос отдельная транзакция. Для нескольких операций атомарно:
try (Connection c = ds.getConnection()) {
c.setAutoCommit(false);
try {
debit(c, from, amount);
credit(c, to, amount);
c.commit();
} catch (SQLException e) {
c.rollback();
throw e;
}
}
Все операции транзакции должны идти через одно и то же соединение. Spring @Transactional делает ровно это: берёт соединение, выключает autocommit и кладёт соединение в ThreadLocal, чтобы репозитории нашли его.
Уровень изоляции задаётся c.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ). Стандартные уровни: READ UNCOMMITTED, READ COMMITTED (по умолчанию в PostgreSQL), REPEATABLE READ (по умолчанию в MySQL InnoDB), SERIALIZABLE. Savepoint позволяет откатить часть транзакции.
Пул соединений
Открытие соединения это TCP, TLS, аутентификация: десятки миллисекунд. Пул держит готовые соединения. HikariCP стоит по умолчанию в Spring Boot. Важные настройки:
maximumPoolSize(по умолчанию 10): больше не всегда лучше, база тоже ограничена по ядрам и соединениям;connectionTimeout: сколько ждать свободного соединения, прежде чем упасть;leakDetectionThreshold: лог, если соединение держат дольше порога.
Когда пул исчерпан, потоки ждут соединения, и сервис встаёт, хотя CPU свободен. Частая причина: долгие транзакции с внешними вызовами внутри.
Батчи
ps.addBatch(); // в цикле
int[] counts = ps.executeBatch();
Пачка вставок за один сетевой обмен в разы быстрее. Для PostgreSQL ещё помогает reWriteBatchedInserts=true в URL.
Что спросят на собесе
- Чем PreparedStatement лучше Statement? Как работает защита от SQL-инъекций?
- Как выполнить две операции в одной транзакции на чистом JDBC?
- Зачем пул соединений, что будет при его исчерпании?
- Уровни изоляции и какие аномалии они допускают.
- Почему важно закрывать Connection и ResultSet?