PreparedStatement
PreparedStatement отделяет SQL-шаблон от значений параметров. Driver передаёт значения согласно протоколу БД, поэтому кавычки внутри значения не превращаются в SQL-код.
Запрос
String sql = """
select id, username
from app_user
where status = ?
and created_at >= ?
order by id
limit ?
""";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "ACTIVE");
statement.setTimestamp(2, Timestamp.from(since));
statement.setInt(3, 100);
try (ResultSet result = statement.executeQuery()) {
while (result.next()) {
long id = result.getLong("id");
String username = result.getString("username");
}
}
}Индексы параметров начинаются с 1. Для nullable значения лучше указывать SQL type:
statement.setNull(1, Types.VARCHAR);Update и generated key
String sql = "insert into app_user(username) values (?)";
try (PreparedStatement statement = connection.prepareStatement(
sql,
Statement.RETURN_GENERATED_KEYS
)) {
statement.setString(1, username);
int changed = statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (changed != 1 || !keys.next()) {
throw new IllegalStateException("Insert returned no key");
}
long id = keys.getLong(1);
}
}Transaction
connection.setAutoCommit(false);
try {
debit(connection, fromId, amount);
credit(connection, toId, amount);
connection.commit();
} catch (SQLException ex) {
connection.rollback();
throw ex;
}Оба statements должны использовать тот же connection. Закрытие connection без явного commit не является переносимым способом управления результатом.
Batch
try (PreparedStatement statement =
connection.prepareStatement("insert into tag(name) values (?)")) {
for (String name : names) {
statement.setString(1, name);
statement.addBatch();
}
int[] counts = statement.executeBatch();
}Batch сокращает round trips, но имеет limits по размеру и error semantics driver. Для очень больших наборов разбиваю на chunks.
Что placeholder не умеет
? заменяет значение, но не имя table, column или ASC/DESC. Динамический identifier выбираю из закрытого allowlist:
String orderBy = switch (sort) {
case "name" -> "username";
case "created" -> "created_at";
default -> throw new IllegalArgumentException("Unknown sort");
};Типичные сбои
- Склеивать пользовательское значение в SQL.
- Путать
executeQueryиexecuteUpdate. - Не закрывать ResultSet/Statement/Connection.
- Передать Java null через неподходящий setter.
- Создать новый connection для второго шага одной transaction.
- Считать PreparedStatement защитой динамического имени column.
Вопросы для собеседования
Как PreparedStatement защищает от injection? Значения передаются отдельно от структуры SQL и не интерпретируются как её tokens.
Можно ли параметризовать имя таблицы? Обычно нет; нужен allowlist и формирование только доверенного fragment.
С какого числа идут параметры? С 1.
Зачем batch? Сократить число client-server round trips для однотипных operations.
Где фиксируется transaction? На Connection или управляющем им transaction manager.
См. DAO Data Access Object, SQL инъекции, Транзакции принцип ACID.