PreparedStatement
So far, every example has built SQL by gluing strings together. That works for fixed, hardcoded SQL, but the moment
a value comes from a user — a search box, a login form, a URL parameter — string concatenation becomes dangerous.
PreparedStatement is JDBC's answer: it precompiles the SQL once and lets you bind values afterward, safely.
Statement vs. PreparedStatement
With a plain Statement, you build the full SQL text yourself, including any values, and send the whole
string to the database every time. With a PreparedStatement, you write the SQL once with
? placeholders standing in for values. The database parses and compiles that SQL ahead of time, and each
time you run it you just supply the values for the placeholders — they are never mixed into the SQL text itself.
| Statement | PreparedStatement | |
|---|---|---|
| SQL text | Built fresh each time, values included | Written once with ? placeholders |
| Values | Concatenated into the string | Bound separately with setX() methods |
| Safety | Vulnerable to SQL injection | Safe — values are never treated as SQL |
| Performance | Recompiled every run | Can be precompiled once, reused many times |
The SQL Injection Risk
Here is a genuinely vulnerable login check, built the way the earlier pages built SQL — by concatenating a value straight into the query string:
// DANGEROUS - do not do this
String userInput = request.getParameter("username");
String sql = "SELECT * FROM users WHERE name = '" + userInput + "'";
ResultSet rs = stmt.executeQuery(sql);
If a normal name like Amit is entered, the final SQL is harmless:
SELECT * FROM users WHERE name = 'Amit'. But an attacker doesn't have to enter a name at all. If they
enter ' OR '1'='1 as the "username", the concatenated string becomes:
SELECT * FROM users WHERE name = '' OR '1'='1'
'1'='1' is always true, so this condition matches every row in the table, not just one user.
Depending on what the surrounding code does with the result, this single trick can leak every user's data or let
someone log in without knowing a password. The problem is that the attacker's input was treated as part of the SQL
language itself instead of as a plain data value.
The Safe Version: PreparedStatement
The fix is to write the SQL with a ? placeholder instead of the raw value, then bind the value using a
setter method. The database driver keeps the value strictly as data — it can never be interpreted as SQL syntax,
no matter what characters it contains.
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userInput); // binds safely, never parsed as SQL
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name"));
}
Now, even if userInput is ' OR '1'='1, the driver looks for a row whose name
column literally equals that entire odd string — it matches nothing, instead of matching every row.
setString(), setInt(), and Parameter Positions
Placeholders are filled using a setXxx(position, value) method that matches the value's type —
setString() for text, setInt() for whole numbers, setDouble() for decimals, and
so on. The position is 1-based, counting the ? marks left to right in the SQL.
String sql = "INSERT INTO students (name, age, course) VALUES (?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "Priya"); // 1st ? - name
pstmt.setInt(2, 22); // 2nd ? - age
pstmt.setString(3, "JDBC"); // 3rd ? - course
int rowsInserted = pstmt.executeUpdate();
System.out.println(rowsInserted + " row(s) inserted.");
Reusing One PreparedStatement in a Loop
Because a PreparedStatement's SQL is precompiled, the same object can be reused for many executions
without recompiling the SQL each time. This is especially useful when inserting many rows — create the
PreparedStatement once, then in each loop iteration just rebind the values and execute.
String sql = "INSERT INTO students (name, course) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql); // compiled once
String[] names = {"Amit", "Priya", "Rahul"};
for (String name : names) {
pstmt.setString(1, name);
pstmt.setString(2, "JDBC");
pstmt.executeUpdate(); // just bind + run, no recompiling
}
pstmt.close();
PreparedStatement. It is safer than Statement and, when reused, often faster too.