How to run a parameterized SQL query safely
Short answer
Write ? in the SQL instead of a value and pass the values through a 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"));
}
}
}
}
}Output:
AnnaSeveral parameters
Parameters are numbered from 1 in the order they appear in the query. For modifying queries executeUpdate() returns the number of affected rows:
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());
}
}
}Output:
Updated: 1A list of values in IN
A single ? cannot stand for a list. Generate as many placeholders as there are values:
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));
}
}
}
}
}Output:
Anna
MiaCommon mistake
Building SQL by concatenating strings. Crafted input changes the meaning of the query — this is SQL injection:
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));
}
}
}
}Output:
Logged in as Anna
Logged in as OlegWith a PreparedStatement the same string is just a wrong password, and the query finds no rows. Table and column names cannot be passed as parameters: if they depend on input, pick them from a fixed list of allowed values.
Functions used in this recipe
- PreparedStatement — A precompiled SQL query with ? parameters. Protects against SQL injection.
- ResultSet — The result of a SELECT query: a cursor that walks through the rows one by one.
- DriverManager.getConnection() — Opens a connection to a database by its JDBC URL.