linkedin Skip to Main Content
Just announced: We now support interviewing in spreadsheets!
Back to blog

Using PreparedStatement in JDBC With Examples

Development

The JD®BC (Java database connectivity) API defines a standard interface for Java applications to connect to different relational database (RDBMS) implementations. Most popular relational database vendors provide JDBC-compliant drivers for their databases.

The JDBC API provides the Statement object to execute queries, updates, and DDL statements on data in a database. The PreparedStatement is a subclass of` Statement that provides better security, portability across vendors, and performance.

The prepared in the name comes from the fact that it’s prepared/compiled initially and then reused rather than having to be compiled for each execution.

This post will explain why and how to work with PreparedStatements.

Usage

Since a PreparedStatement is a subclass of Statement, it can do what a Statement can do, plus more.

The fact that the PreparedStatement is pre-compiled is transparent to its users. Where it differs in usage is in its ability to specify parameters.

The JDBC driver handles these parameters and thus, the handling is naturally compatible with the database we’re using. This leads to some benefits, as we will see later.

Below is an example of using the PreparedStatement with two parameters:

PreparedStatement insStm = conn.prepareStatement("insert into emp values(?,?)");

insStm.setInt(1, 1);

insStm.setString(2, "Harry Potter");

insStm.execute();Code language: Java (java)

As shown above, we’re inserting records into the emp table. In the SQL command string, we denote the places where a parameter should be with a question mark. When specifying parameter values, we identify a parameter by its position in the SQL command.

The starting position is 1 instead of the usual 0. If we use the same parameter value in multiple positions, we’ll have to set it for each position.

The first parameter is an integer, so we use the setInt method. The second is a string, so we use setString. Using the strongly typed methods ensures that we won’t pass the wrong type of parameter by mistake.

But if we were getting the parameters dynamically, say, from an array or collection, it would be cumbersome to check the type of each parameter and then use the corresponding set method. The setObject method is helpful here. Using it, we can set a parameter of any supported type.

A PreparedStatement object is not thread-safe; we should not use the same instance concurrently from multiple threads.

Improper usage

We can only use parameters where we would use values. If we try to use parameters to build other parts of the SQL command, it won’t work. For example, what we see here would fail with an SQLSyntaxErrorException:

PreparedStatement pStm = conn.prepareStatement("select * from ?");

pStm.setString(1, "emp");

pStm.execute();Code language: Java (java)

Here, we’re trying to provide the table name as a parameter. However, this isn’t allowed, nor would any other syntax part be, like column names, where clause, group by, order by, etc.

Security

A PreparedStatement provides better security against certain hacking attacks compared with its parent, the Statement. The Statement object executes a string command. If it needs to use some parameters, we must concatenate them as strings with the SQL command. This creates a risk of the command being changed to something else using the parameters.

Consider the query below using string concatenation to validate a user’s login:

"select * from user where user_name = '" + usrName + "'"Code language: SQL (Structured Query Language) (sql)

Here, usrName is a string that the application’s user has provided. If a malicious user were to enter usrName as ' or '1'='1 (note the single quotes used), the query string now becomes

select * from user where user_name = '' or '1'='1'Code language: SQL (Structured Query Language) (sql)

This SQL command now circumvents the check for user_name, since the added OR condition is always true. If the database allows us to run multiple commands in a go (separated by a semicolon, for example), then a hacker could append a destructive command like delete or drop as well. This type of hacking attack using injected parameters to alter the SQL command is called an SQL injection attack.

Now, let’s try using a PreparedStatement instead. Using PreparedStatement, the above code would become

PreparedStatement loginStm = conn.prepareStatement("select * from user where user_name = ?");

loginStm.setInt(1, usrName);

loginStm.execute();Code language: Java (java)

What’s different here? The JDBC driver is handling the parameters. It’ll send the parameters to the database as variables bound to the pre-compiled SQL command. The database will escape the special character within them if needed. For example, it usually escapes a single quote with an additional single quote. Thus, the malicious value remains a value, and it cannot change the SQL command.

Note that we can use a Statement and escape the values ourselves before concatenating, but the escaping syntax can differ from database to database and hence, it’s best to let the JDBC driver handle it.

Also, there are still other advantages to using a PreparedStatement. Read on.

Portability and ease of use

When we use string concatenation to inject parameters into the SQL command, we have to format and escape the values too. Also, this part could be database specific, making our code less portable across different database implementations.

We’ve seen an example of this in the SQL injection section above. Parameters in PreparedStatement make our life easier and keep the code portable since the JDBC driver handles them.

Let’s see another example using dates. Let’s assume the user has entered a date and wants to filter records by that date. If we were building the query as a string, then we would need to convert the date into a string:

"select * from sales where sale_date > '" + user_date_string + "'"Code language: SQL (Structured Query Language) (sql)

What should be the format of the date string? For example, should it be dd-MM-YY or MM-dd-YY, etc.?

This would work only if the format of user_date_string matches the default date format for the database. The default date format can vary from one database instance to another, so we cannot be sure about it.

We could use some database function like TO_DATE that will parse the user_date_string with the given format and convert it back into the date.

"select * from sales where sale_date > TO_DATE( 'DD-MM-YYYY', '" + user_date_string + "')"Code language: SQL (Structured Query Language) (sql)

However, the syntax of functions like TO_DATE is specific to a database vendor; others might not have the same name or parameters. Some functions are supported by the JDBC spec using the fn keyword, but many JDBC drivers don’t have implementations for these.

So, it’s PreparedStatement to the rescue:

PreparedStatement dateFilterStm = conn.prepareStatement("select * from sales where sale_date > ?)";

dateFilterStm.setDate( 1, usrDate);Code language: Java (java)

All we need to do is pass the date as java.util.Date or java.sql.Date to the PreparedStatement. There’s no worrying about escaping or formatting or portability.

PreparedStatement rocks!

Performance

Pre-compilation

We mentioned at the start that the SQL command used in a PreparedStatement is pre-compiled or prepared initially and then reused.

What does pre-compile mean, and who does it? When we run an SQL command on the database, the database will first validate, parse, and build an execution plan to run it. These steps come under pre-compiling.

When the JDBC driver creates a prepared statement, it will ask the database to pre-compile that SQL command. Then, the database will store the pre-compiled command in a cache for the current database connection. If the same SQL command is to be run multiple times, using the pre-compiled version is better for performance since the compilation part has to be done only once.

The database has a limited cache for pre-compiled SQL commands and hence, we cannot have too many of them. Defining parameters helps make the command reusable and therefore reduces the number of distinct commands.

If the parameters weren’t separate, the same command with different parameters would each become a separate command.

For example:

select * from emp where name = 'Goldstein';

select * from emp where name = 'Mukherjee';

select * from emp where name = ?;Code language: Java (java)

In the above examples, the first two SQL commands are distinct since we didn’t define the parameters separately. Whereas, if we had used a PreparedStatement as in the third, then they could have used the same SQL command.

Reusing

Using the same PreparedStatement multiple times in the same database session/connection will be better for performance. Each time, we need to set the parameters and execute the PreparedStatement. For example, we could use it to repeatedly insert data into a table

PreparedStatement insStm = conn.prepareStatement("insert into emp values(?,?)");

insStm.setInt(1, 1);

insStm.setString(2, "Harry Potter");

insStm.execute();

insStm.setInt(1, 2);

insStm.setString(2, "Tom Riddle");

insStm.execute();

insStm.close();Code language: Java (java)

or we could do the set parameters and execute() inside a loop. This is still not the best solution since each execute() will mean a round trip to the database.

Batching

We can send n number of statements in a single batch to the database, where n is up to us to decide. This will reduce the round trips and greatly improve performance. Below is an example using batching.

PreparedStatement insStm = conn.prepareStatement("insert into emp values(?,?)");

List<String> names = List.of( "Wareen Buffet", "Steve Jobs", "Bill Gates");

boolean moreRows = false;

int i=0;

for( i=0; i< names.size(); i++) {

  insStm.setInt(1, i+1);
  
  insStm.setString(2,names.get(i));

  insStm.addBatch();

  if( i % 100 == 0) {

    // execute every 100 rows

    int[] rowCounts = insStm.executeBatch();
  }

}

if( i % 100 != 0) {

  // lef tover rows

  int[] rowCounts = insStm.executeBatch();

}

insStm.close();Code language: Java (java)

Here, we call set parameters and addBatch() for each row in the loop. The addBatch() will store the current parameters for later execution.

When the counter is a multiple of 100, we’ll execute the batched rows in one go. In our case, that means 100 at a time.

executeBatch() returns an array of integers corresponding to the number of rows affected by each statement in the batch. The SQL command needs to be sent to the database only once for pre-compilation. The batch needs to send a reference to the pre-compiled command and the parameters for all the rows in the batch.

Conclusion

In this post, we’ve seen when and why we should use PreparedStatements, along with examples using code.

PreparedStatements are the preferred way to execute SQL commands using the JDBC API since they offer better security, ease of use, portability, and performance.

This post was written by Manoj Mokashi. Manoj has more than 25 years of experience as a developer, mostly on java, web technologies, and databases. He’s also worked on PHP, Python, Spark, AI/ML, and Solr and he really enjoys learning new things.