Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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
| Критерий | Statement | PreparedStatement |
|---|---|---|
| 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
-
Всегда для пользовательского ввода
// ✅ Всегда используй PreparedStatement с параметрами pstmt.setString(1, userInput); -
Для повторяющихся запросов
- Кэшируется план выполнения
- Экономит ресурсы процессора на БД
-
Для обработки чувствительных данных
- Пароли, 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 запросов!