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.