← Назад к вопросам

Что делает PreparedStatement?

1.0 Junior🔥 171 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

PreparedStatement: назначение и работа

PreparedStatement — это один из самых важных инструментов для безопасной работы с БД в Java. Это не просто обёртка над простыми SQL запросами, а специальный механизм с параметризованными запросами.

Основная идея

PreparedStatement позволяет отделить SQL-структуру запроса от данных и защитить от SQL-injection атак.

// ❌ ОПАСНО: Statement (уязвимо для SQL injection)
String username = "admin'; DROP TABLE users; --";
String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
// Query: SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
// Таблица может быть удалена!

// ✅ БЕЗОПАСНО: PreparedStatement (защищено)
String username = "admin'; DROP TABLE users; --";
String query = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username); // Параметр экранируется автоматически
ResultSet rs = pstmt.executeQuery();
// Параметр передаётся как данные, не как код SQL

Как работает PreparedStatement

Шаг 1: Подготовка (Prepare)

// SQL запрос отправляется БД-серверу
String query = "SELECT * FROM users WHERE id = ? AND status = ?";
PreparedStatement pstmt = connection.prepareStatement(query);

// На сервере DB:
// 1. Парсирует SQL синтаксис
// 2. Создаёт план выполнения (execution plan)
// 3. Сохраняет на сервере
// 4. Ждёт параметров

Шаг 2: Установка параметров (Bind)

// Параметры передаются отдельно
pstmt.setInt(1, 123); // Первый ?
pstmt.setString(2, "active"); // Второй ?

// На сервере DB: параметры подставляются в план выполнения
// SQL структура уже закреплена

Шаг 3: Выполнение (Execute)

ResultSet rs = pstmt.executeQuery();
// На сервере выполняется уже подготовленный план

Преимущества PreparedStatement

1. Защита от SQL Injection

Самое важное преимущество!

// Даже если пользователь введёт опасный SQL
String userInput = "1; DELETE FROM users;";
String query = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, userInput);
ResultSet rs = pstmt.executeQuery();

// На БД выполняется: SELECT * FROM users WHERE id = '1; DELETE FROM users;'
// Это просто строка, DELETE не выполняется

2. Лучшая производительность

План выполнения кэшируется на сервере БД.

// Первый запрос: парсирование + компиляция + выполнение
PreparedStatement pstmt = connection.prepareStatement(
    "SELECT * FROM users WHERE id = ?"
);

// Повторные запросы: только подстановка параметров + выполнение
for (int i = 1; i <= 1000; i++) {
    pstmt.setInt(1, i);
    ResultSet rs = pstmt.executeQuery();
    // На сервере уже закэшпирован план выполнения
}
// Намного быстрее чем 1000 отдельных Statement запросов

3. Автоматическое экранирование

Особые символы автоматически экранируются.

// Пользователь введёт строку с кавычками
String text = "It's a string";
pstmt.setString(1, text);
// Параметр экранируется автоматически: "It\'s a string"
// Не нужно вручную экранировать

Способы установки параметров

String query = "INSERT INTO users (id, name, email, salary, active, joined) VALUES (?, ?, ?, ?, ?, ?)";
PreparedStatement pstmt = connection.prepareStatement(query);

// setInt() — целые числа
pstmt.setInt(1, 123);

// setString() — текст
pstmt.setString(2, "John Doe");

// setString() для email
pstmt.setString(3, "john@example.com");

// setBigDecimal() — для денег
pstmt.setBigDecimal(4, new BigDecimal("5000.50"));

// setBoolean() — логические значения
pstmt.setBoolean(5, true);

// setDate() — даты
pstmt.setDate(6, new java.sql.Date(System.currentTimeMillis()));

// setNull() — NULL значения
pstmt.setNull(7, java.sql.Types.VARCHAR);

// setObject() — универсальный способ
pstmt.setObject(8, someObject);

int rowsInserted = pstmt.executeUpdate();

Пример: SELECT с параметрами

public User findUserById(Long id) throws SQLException {
    String query = "SELECT id, name, email, created_at FROM users WHERE id = ?";
    
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(query)) {
        
        pstmt.setLong(1, id); // Установка параметра
        
        try (ResultSet rs = pstmt.executeQuery()) {
            if (rs.next()) {
                return new User(
                    rs.getLong("id"),
                    rs.getString("name"),
                    rs.getString("email"),
                    rs.getTimestamp("created_at")
                );
            }
        }
    }
    return null; // Пользователь не найден
}

Пример: INSERT с несколькими строками

public void insertUsers(List<User> users) throws SQLException {
    String query = "INSERT INTO users (name, email) VALUES (?, ?)";
    
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(query)) {
        
        for (User user : users) {
            pstmt.setString(1, user.getName());
            pstmt.setString(2, user.getEmail());
            pstmt.addBatch(); // Добавляем в batch
        }
        
        int[] results = pstmt.executeBatch(); // Выполняем все одновременно
        System.out.println("Inserted: " + results.length + " users");
    }
}

Пример: UPDATE с условиями

public void updateUserEmail(Long userId, String newEmail) throws SQLException {
    String query = "UPDATE users SET email = ? WHERE id = ?";
    
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(query)) {
        
        pstmt.setString(1, newEmail);
        pstmt.setLong(2, userId);
        
        int rowsAffected = pstmt.executeUpdate();
        if (rowsAffected > 0) {
            System.out.println("User email updated successfully");
        }
    }
}

Пример: DELETE с условиями

public void deleteInactiveUsers(int days) throws SQLException {
    String query = "DELETE FROM users WHERE last_login < DATE_SUB(NOW(), INTERVAL ? DAY)";
    
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(query)) {
        
        pstmt.setInt(1, days);
        
        int deletedCount = pstmt.executeUpdate();
        System.out.println("Deleted inactive users: " + deletedCount);
    }
}

Batch операции для производительности

public void batchInsertUsers(List<User> users) throws SQLException {
    String query = "INSERT INTO users (name, email, status) VALUES (?, ?, ?)";
    
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(query)) {
        
        // Отключаем auto-commit для batch операции
        conn.setAutoCommit(false);
        
        for (User user : users) {
            pstmt.setString(1, user.getName());
            pstmt.setString(2, user.getEmail());
            pstmt.setString(3, "active");
            pstmt.addBatch();
            
            // Выполняем batch каждые 1000 записей
            if (users.indexOf(user) % 1000 == 0) {
                pstmt.executeBatch();
            }
        }
        
        pstmt.executeBatch(); // Финальный batch
        conn.commit(); // Подтверждаем транзакцию
    } catch (SQLException e) {
        conn.rollback(); // Откатываем при ошибке
        throw e;
    }
}

Сравнение Statement vs PreparedStatement

КритерийStatementPreparedStatement
SQL InjectionУязвимоЗащищено
ПроизводительностьМедленнее (каждый запрос парсируется)Быстрее (кэшируемый план)
ПереиспользованиеНеэффективноЭффективно
ЭкранированиеРучноеАвтоматическое
ТипобезопасностьНетДа (setInt, setString и т.д.)
СложностьПростойНемного сложнее

Современный подход: ORM

// В современных приложениях используются ORM (Hibernate, JPA)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findById(Long id); // Использует PreparedStatement под капотом
}

// Spring Data JPA автоматически создаёт PreparedStatement

Когда использовать PreparedStatement

  1. Всегда для пользовательского ввода

    // ✅ Всегда используй PreparedStatement с параметрами
    pstmt.setString(1, userInput);
    
  2. Для повторяющихся запросов

    • Кэшируется план выполнения
    • Экономит ресурсы процессора на БД
  3. Для обработки чувствительных данных

    • Пароли, API ключи, персональные данные
    • PreparedStatement гарантирует их безопасность

Итоговый пример: полноценный DAO

@Component
public class UserDao {
    private final DataSource dataSource;
    
    public UserDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    public User findById(Long id) throws SQLException {
        String query = "SELECT * FROM users WHERE id = ?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(query)) {
            
            pstmt.setLong(1, id);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return mapRowToUser(rs);
                }
            }
        }
        return null;
    }
    
    public List<User> findByStatus(String status) throws SQLException {
        String query = "SELECT * FROM users WHERE status = ? ORDER BY created_at DESC";
        List<User> users = new ArrayList<>();
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(query)) {
            
            pstmt.setString(1, status);
            try (ResultSet rs = pstmt.executeQuery()) {
                while (rs.next()) {
                    users.add(mapRowToUser(rs));
                }
            }
        }
        return users;
    }
    
    private User mapRowToUser(ResultSet rs) throws SQLException {
        return new User(
            rs.getLong("id"),
            rs.getString("name"),
            rs.getString("email"),
            rs.getTimestamp("created_at")
        );
    }
}

Заключение

PreparedStatement — это не просто механизм работы с БД, это стандарт безопасности для Java приложений:

  • Защита от SQL Injection
  • Лучшая производительность
  • Автоматическое экранирование
  • Типобезопасность

Никогда не используй конкатенацию строк для построения SQL запросов!

Что делает PreparedStatement? | PrepBro