Урок по треку Java 6 мин

JDBC, пул соединений и транзакции

Connection, PreparedStatement, пул соединений, транзакции, уровни изоляции и SQL-инъекции.

Даже если в проекте 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?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.