Как правило, запросы в базу формируются динамически на основе данных, которые поступают извне. Например, создание пользователя чаще всего происходит во время регистрации. В таком случае код вставки выглядит так:
var name = ...;
var phone = ...;
var sql = "INSERT INTO users (username, phone) VALUES ('" + name + "', '" + phone + "')";
try (var statement = conn.createStatement()) {
statement.executeUpdate(sql);
}
Составленные таким образом запросы несут в себе серьезную опасность — SQL-инъекции. Вместо обычных данных злоумышленник может отправить текст, который изменит SQL-запрос. Представьте, что внутри переменной хранилась бы такая строка:
'); INSERT INTO users (username, phone) VALUES ('i am hacker', '777777);
В таком случае после подстановки мы бы получили следующий запрос:
INSERT INTO users (username, phone) VALUES ('somename', '');
INSERT INTO users (username, phone) VALUES ('i am hacker', '777777');
Фактически, вместо одного запроса получилось два. Таким образом, злоумышленник может выполнить произвольный запрос в базу данных — то есть удалить, добавить или изменить все, что ему хочется.
В разработке существует правило «Никогда не доверяй пользовательским данным» — оно особенно важно в работе с базами данных. Любые данные перед вставкой нужно экранировать с помощью подготовленных запросов:
var sql = "INSERT INTO users (username, phone) VALUES (?, ?)";
try (var preparedStatement = conn.prepareStatement(sql)) {
preparedStatement.setString(1, "Tommy");
preparedStatement.setString(2, "33333333");
preparedStatement.executeUpdate();
}
Подготовленные запросы создаются с помощью плейсхолдеров. Это заполнители, которые расставляются в местах, где ожидается подстановка данных. При этом плейсхолдеры не нужно оборачивать в кавычки, если это строки. JDBC работает так:
- Автоматически выполняет подстановку
- Создает стейтмент
preparedStatement
- Передает в него подготовленный SQL-шаблон
- Заполняет шаблон данными
- Вызывает его
Заполнение данными выполняется с помощью методов, похожих на те, которые используются в ResultSet
. Разница только в том, что здесь конвертация происходит в обратную сторону. Каждый метод наподобие setString()
принимает на вход два параметра:
- Порядковый номер плейсхолдера
- Данные, которые нужно подставить
Ускорение запросов
Кроме того, preparedStatement
может ускорять запросы. Дело в том, что мы можем выполнять множество запросов подряд в рамках одного preparedStatement
. За счет этого запрос готовится предварительно, что ускоряет работу:
var sql = "INSERT INTO users (username, phone) VALUES (?, ?)";
try (var preparedStatement = conn.prepareStatement(sql)) {
preparedStatement.setString(1, "Tommy");
preparedStatement.setString(2, "33333333");
preparedStatement.executeUpdate();
preparedStatement.setString(1, "Maria");
preparedStatement.setString(2, "44444444");
preparedStatement.executeUpdate();
}
Возврат идентификатора
Когда мы вставляем данные в базу, иногда нам нужно получить идентификатор вставленной записи и потом использовать его в коде. Это происходит, когда мы создаем какую-то сущность и хотим потом ее использовать:
var user = new User();
// Сохраняем пользователя в базу данных
// После этого становится доступен id
user.getId();
// Его можно использовать для формирования ссылок или вставки связанных записей
Чтобы выполнить эту задачу, немного изменим запрос:
// Импортируем
import java.sql.Statement;
var sql = "INSERT INTO users (username, phone) VALUES (?, ?)";
try (var preparedStatement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setString(1, "Sarah");
preparedStatement.setString(2, "333333333");
preparedStatement.executeUpdate();
// Если ключ составной, значений может быть несколько
// В нашем случае, ключ всего один
var generatedKeys = preparedStatement.getGeneratedKeys();
if (generatedKeys.next()) {
System.out.println(generatedKeys.getLong(1));
} else {
throw new SQLException("DB have not returned an id after saving the entity");
}
}
Самостоятельная работа
- Выполните шаги из урока у себя на компьютере
- Добавьте в базу данных несколько пользователей, используя один
preparedStatement
- Используйте
preparedStatement
и добавьте в приложение запрос, который удалит одного из созданных пользователей по его имени
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.