Как безопасно выполнить SQL-запрос с параметрами
Короткий ответ
Пишите в SQL знак ? вместо значения и передавайте значения через PreparedStatement:
import java.sql.*;
void main() throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
try (Statement st = conn.createStatement()) {
st.executeUpdate("CREATE TABLE users (name TEXT, city TEXT)");
st.executeUpdate("INSERT INTO users VALUES ('Anna', 'Paris'), ('Oleg', 'Berlin')");
}
String city = "Paris";
try (PreparedStatement ps = conn.prepareStatement("SELECT name FROM users WHERE city = ?")) {
ps.setString(1, city);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
IO.println(rs.getString("name"));
}
}
}
}
}Результат:
AnnaНесколько параметров
Параметры нумеруются с 1 в порядке появления в запросе. Для изменяющих запросов executeUpdate() возвращает число затронутых строк:
import java.sql.*;
void main() throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
try (Statement st = conn.createStatement()) {
st.executeUpdate("CREATE TABLE products (title TEXT, price REAL, stock INTEGER)");
st.executeUpdate("INSERT INTO products VALUES ('Tea', 3.5, 10), ('Cake', 6.0, 0)");
}
try (PreparedStatement ps = conn.prepareStatement(
"UPDATE products SET price = price * ? WHERE stock > ?")) {
ps.setDouble(1, 0.9);
ps.setInt(2, 0);
IO.println("Updated: " + ps.executeUpdate());
}
}
}Результат:
Updated: 1Список значений в IN
Один ? не может заменить список. Создайте столько знаков, сколько значений:
import java.sql.*;
void main() throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
try (Statement st = conn.createStatement()) {
st.executeUpdate("CREATE TABLE users (id INTEGER, name TEXT)");
st.executeUpdate("INSERT INTO users VALUES (1, 'Anna'), (2, 'Oleg'), (3, 'Mia')");
}
List<Integer> ids = List.of(1, 3);
String marks = String.join(", ", Collections.nCopies(ids.size(), "?"));
String sql = "SELECT name FROM users WHERE id IN (" + marks + ") ORDER BY id";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
for (int i = 0; i < ids.size(); i++) {
ps.setInt(i + 1, ids.get(i));
}
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
IO.println(rs.getString(1));
}
}
}
}
}Результат:
Anna
MiaЧастая ошибка
Склеить SQL из строк. Специально подобранный ввод меняет смысл запроса — это SQL-инъекция:
import java.sql.*;
void main() throws SQLException {
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:");
Statement st = conn.createStatement()) {
st.executeUpdate("CREATE TABLE users (name TEXT, password TEXT)");
st.executeUpdate("INSERT INTO users VALUES ('Anna', 'a1'), ('Oleg', 'o2')");
String password = "x' OR '1'='1";
String sql = "SELECT name FROM users WHERE password = '" + password + "'";
try (ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
IO.println("Logged in as " + rs.getString(1));
}
}
}
}Результат:
Logged in as Anna
Logged in as OlegС PreparedStatement та же строка — просто неверный пароль, и запрос не найдёт ни одной строки. Имена таблиц и столбцов параметром передать нельзя: если они зависят от ввода, выбирайте их из заранее известного списка.
Функции из этого ответа
- PreparedStatement — Заранее подготовленный SQL-запрос с параметрами ?. Защищает от SQL-инъекций.
- ResultSet — Результат SELECT-запроса: курсор, который по одной перебирает строки таблицы.
- DriverManager.getConnection() — Открывает соединение с базой данных по JDBC-адресу.