Chapter FOURTEEN
Accessing Databases with JDBC


Exam Objectives

Create connections, create and execute basic, prepared and callable statements, process query results and control transactions using JDBC API.

Chapter Content


Introduction to JDBC

Java Database Connectivity (JDBC) is an API for accessing databases from Java. In simple terms, JDBC is like a universal adapter that allows your Java programs to plug into and interact with virtually any relational database. If Java is the common language that lets you write a program once and run it anywhere there’s a Java Virtual Machine, JDBC is the common language that lets that program talk to any database.

Here’s a diagram that shows the overall JDBC architecture:

┌─────────────────────────────────────────────────────────┐
│                   JDBC Architecture                     │
│                                                         │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │    Java     │    │    JDBC     │    │  Database   │  │
│  │ Application │◄──►│    API      │◄──►│   Driver    │  │
│  └─────────────┘    └─────────────┘    └──────┬──────┘  │
│                                               │         │
│                                               │         │
│                                        ┌──────▼──────┐  │
│                                        │  Database   │  │
│                                        │             │  │
│                                        └─────────────┘  │
│                                                         │
│  Key Components:                                        │
│  1. Java Application: Uses JDBC API                     │
│  2. JDBC API: Provides standard database access method  │
│  3. JDBC Driver: Implements the JDBC API for a specific │
│     database                                            │
│  4. Database: The actual database system                │
│                                                         │
└─────────────────────────────────────────────────────────┘

The JDBC API defines a set of Java interfaces to support this architecture:

Almost all the methods of these core JDBC interfaces declare that they may throw an SQLException. This is a checked exception that provides information on a database access error or other errors.

An SQLException provides the following useful methods:

The following diagram shows the workflow of JDBC:

┌─────────────────────────────────────────────────────────┐
│                    JDBC Workflow                        │
│                                                         │
│  ┌─────────────┐                                        │
│  │  Load JDBC  │                                        │
│  │   Driver    │                                        │
│  └──────┬──────┘                                        │
│         │                                               │
│         ▼                                               │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │ Establish   │    │   Create    │    │  Execute    │  │
│  │ Connection  │───►│  Statement  │───►│   Query     │  │
│  └──────┬──────┘    └─────────────┘    └──────┬──────┘  │
│         │                                     │         │
│         │                              ┌──────▼──────┐  │
│         │                              │  Process    │  │
│         │                              │  ResultSet  │  │
│         │                              └──────┬──────┘  │
│         │                                     │         │
│         │           ┌─────────────┐    ┌──────▼──────┐  │
│         └───────────┤    Close    │◄───┤    Close    │  │
│                     │ Connection  │    │  ResultSet  │  │
│                     └─────────────┘    └─────────────┘  │
│                                                         │
│                                                         │
└─────────────────────────────────────────────────────────┘

A JDBC driver is a software component that enables Java applications to interact with a database. It converts JDBC calls into the specific database protocol required to connect and interact with the database.

To load a JDBC driver, you need to include the driver class (JAR) in your application’s classpath. For JDBC 4.0 and later (Java 6 and above), the driver is automatically loaded if the JAR file of the driver is present in the classpath, you don’t need to explicitly load the driver class in your code.

So let’s start with the JDBC URL and the Connection object.

Connecting to a Database

The JDBC URL contains all the necessary information to locate and access the database, neatly packaged into a single string.

The format is straightforward:

jdbc:{driver-name}://{server-name}:{port}/{database-name}

For example, to connect to an Oracle database named users on localhost, the JDBC URL might look like:

jdbc:oracle:thin:@localhost:1521:users

With the JDBC URL and the database credentials, we can obtain a Connection object, either through the java.sql.DriverManager utility class or a javax.sql.DataSource object.

DriverManager is the simpler, more direct path. You load the database driver class, pass the JDBC URL and credentials to DriverManager.getConnection(), and you get a Connection object:

String url = "jdbc:mydb://localhost:1234/users";
Connection conn = DriverManager.getConnection(url, "username", "password");

DataSource is a slightly more involved path, designed for use in application servers. You configure a DataSource object with the database information and let the application server manage the connections:

Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/myDB");
Connection conn = ds.getConnection();

In any case, once you have a Connection object, you’re ready to start executing SQL statements and retrieving results.

Executing Queries

In JDBC, the interaction with the database centers around three types of statements:

For one-off, straightforward SQL queries, a simple Statement object will suffice. You can create a Statement from your Connection object with:

Statement stmt = conn.createStatement();

And then execute your SQL using one of three methods:

Here’s a quick example:

Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");

But simple Statements have their limitations. They’re not very efficient for repeated queries, and they don’t allow for parameterization. Here’s where PreparedStatement comes in.

Think of a PreparedStatement as a template for an SQL query. You define the basic structure of the query, with placeholders for parameters, and the database precompiles it. Then you can efficiently execute that query multiple times with different parameter values.

Obtaining a PreparedStatement is similar to getting a regular Statement, but with your SQL template as a parameter:

String sql = "UPDATE users SET status = ? WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);

Those ? symbols are the placeholders for parameters.

It’s generally better to use bind variables (the ? placeholders) instead of concatenating values into your SQL string. This has several benefits:

  1. It helps prevent SQL injection attacks. If you concatenate user input directly into your SQL, a malicious user could inject their own SQL commands. Bind variables avoid this risk.

  2. It improves performance. When you use bind variables, the database can reuse the parsed and optimized SQL statement, even if the parameter values change. If you concatenate values into the SQL, the database has to parse and optimize the statement every time.

Now, executing a PreparedStatement is a little different. Instead of passing the SQL to the execute method, you first set the parameter values using setter methods like setString(), setInt(), etc. The parameters are numbered starting from 1.

pstmt.setString(1, "active");
pstmt.setInt(2, 1234);

Then you call one of the execution methods (executeQuery(), executeUpdate(), or execute()) just like with a simple Statement.

int rowCount = pstmt.executeUpdate();

But what happens if we use the wrong execution method for our SQL statement? For example, what if we use executeQuery() for an UPDATE statement?

You’ll receive an SQLException. As a rule of thumb:

One of the key benefits of PreparedStatements is the ability to batch multiple statements together for execution. This can dramatically improve performance when you need to execute many similar statements.

Suppose you need to insert a thousand rows into the users table. With simple Statements, you’d have to execute a thousand individual INSERT statements. But with PreparedStatements and batching, you can do it much more elegantly:

PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users VALUES (?, ?)");
for (int i = 0; i < 1000; i++) {
    pstmt.setString(1, "user" + i);
    pstmt.setString(2, "password" + i);
    pstmt.addBatch();
}
int[] rowCounts = pstmt.executeBatch();

The addBatch() method adds the current set of parameters to the batch, and executeBatch() executes all the accumulated statements in one fell swoop.

For even more power and flexibility, JDBC allows you to call stored procedures in your database using CallableStatements. Stored procedures can have IN, OUT, and INOUT parameters, and CallableStatements handle them all.

To call a procedure with no parameters, the process is straightforward:

CallableStatement cstmt = conn.prepareCall("{CALL procedureName}");
cstmt.execute();

For procedures with parameters, use the setter and getter methods corresponding to the parameter types. Let’s look at examples for IN, OUT, and INOUT parameters.

Passing an IN parameter:

CallableStatement cstmt = conn.prepareCall("{CALL procedureName(?)}");
cstmt.setString(1, "value");
cstmt.execute();

Getting an OUT parameter:

CallableStatement cstmt = conn.prepareCall("{CALL procedureName(?)}");
cstmt.registerOutParameter(1, java.sql.Types.VARCHAR);
cstmt.execute();
String result = cstmt.getString(1);

And for an INOUT parameter, you use both a setter and a getter:

CallableStatement cstmt = conn.prepareCall("{CALL procedureName(?)}");
cstmt.setString(1, "inValue");
cstmt.registerOutParameter(1, java.sql.Types.VARCHAR); 
cstmt.execute();
String result = cstmt.getString(1);

Retrieving Results from a ResultSet

While Statements and PreparedStatements are about sending data and commands to the database, ResultSets are all about retrieving data from the database. They represent the rows and columns returned by a SQL query.

Reading a ResultSet is like iterating over a 2D array, you have a cursor that moves from row to row, and, for each row, you can access the columns by index or by name. The cursor starts before the first row, so you need to call next() to move to the first row.

Here’s a basic example:

ResultSet rs = stmt.executeQuery("SELECT id, name FROM users");
while (rs.next()) {
    int id = rs.getInt("id");
    String name = rs.getString("name");
    // do something with the data
}

The ResultSet provides getter methods for all the common data types: getInt(), getString(), getDouble(), getDate(), etc. You can retrieve a column value by its index (starting from 1) or by its name.

By default, ResultSets are forward-only and read-only. This means you can only move forward through the rows using next(), and you can’t update the data in the ResultSet. However, JDBC allows you to create ResultSets with different capabilities by passing type and concurrency options to the createStatement() or prepareStatement() methods.

The type options are:

The concurrency options are:

Here’s an example of creating a scrollable, updatable ResultSet:

Statement stmt = conn.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE, 
    ResultSet.CONCUR_UPDATABLE);

ResultSet rs = stmt.executeQuery("SELECT * FROM users");

// Move to the last row
rs.last();

// Update the name in the current row
rs.updateString("name", "New Name");
rs.updateRow();

These options give you a lot of flexibility in how you work with the data retrieved from your database.

Using Transactions

Imagine you’re transferring money from one bank account to another. This involves two steps: debiting the amount from the source account, and crediting it to the destination account. Now, what if something goes wrong after the debit but before the credit? You’d have an inconsistency in your data and money would be lost!

This is where transactions come in. A transaction is a sequence of database operations that are treated as a single unit of work. Either all of the operations are completed (committed), or none of them are (rolled back). This ensures that the database is always left in a consistent state.

Here’s a diagram that shows the typical workflow of a transaction in JDBC:

┌─────────────────────────────────────────────────────────┐
│                 JDBC Transaction                        │
│                                                         │
│  ┌─────────────┐                                        │
│  │ Begin       │                                        │
│  │ Transaction │                                        │
│  └──────┬──────┘                                        │
│         │                                               │
│         ▼                                               │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │   SQL       │    │    SQL      │    │    SQL      │  │
│  │ Statement 1 │───►│ Statement 2 │───►│ Statement 3 │  │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘  │
│         │                  │                  │         │
│         │          ┌───────▼───────┐          │         │
│         │          │   Savepoint   │          │         │
│         │          └───────┬───────┘          │         │
│         │                  │                  │         │
│         └──────────────────┴──────────────────┘         │
│                            │                            │
│                     ┌──────▼──────┐                     │
│                     │   Commit/   │                     │
│                     │  Rollback   │                     │
│                     └─────────────┘                     │
│                                                         │
│  Key Concepts:                                          │
│  1. Transactions group multiple operations              │
│  2. Savepoints allow partial rollback                   │
│  3. Commit makes changes permanent                      │
│  4. Rollback undoes all changes in the transaction      │
│                                                         │
└─────────────────────────────────────────────────────────┘

In JDBC, every Connection has an associated transaction. By default, each SQL statement is automatically committed as soon as it finishes. This is known as auto-commit mode. However, you can disable auto-commit and manage the transaction manually.

To start a transaction, you disable auto-commit on the Connection:

conn.setAutoCommit(false);

From this point on, no SQL statements will be committed until you explicitly call the commit() method:

conn.commit();

If at any point you decide to abort the transaction, you can call the rollback() method, which will undo all the changes made since the start of the transaction:

conn.rollback();

It’s important to always either commit or roll back a transaction. If you disable auto-commit, start a transaction, and then close the Connection without committing or rolling back, the behavior will be undefined. The changes might be committed, or they might be rolled back, or they might be left in limbo, depending on the database and the JDBC driver. To avoid this uncertainty always explicitly commit or roll back before closing the Connection.

Here’s a typical pattern for using transactions:

try {
    conn.setAutoCommit(false);
    // execute some SQL statements
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
} finally {
    conn.setAutoCommit(true);
}

The finally block ensures that auto-commit is re-enabled, even if an exception occurs.

Savepoints

Sometimes, you might want to partially roll back a transaction. For example, suppose a transaction consists of three SQL statements. After executing the first two statements, you encounter an error. You want to roll back the second statement, but keep the changes made by the first statement.

This is where savepoints come in. A savepoint is a named bookmark within a transaction. You can roll back to a savepoint without aborting the entire transaction.

Here’s how it works:

Savepoint sp1 = conn.setSavepoint();
// execute first statement

Savepoint sp2 = conn.setSavepoint();
// execute second statement

if (error) {
    conn.rollback(sp2);
}

// execute third statement

conn.commit();

In this example, if an error occurs after the second statement, we roll back to the sp2 savepoint, which discards the changes made by the second statement but keeps the changes made by the first statement. We then proceed with the third statement and commit the transaction.

Transactions are a powerful tool for ensuring data integrity, but they also come with some overhead. Opening a transaction, committing it, and rolling it back all involve communication with the database server, which can be slow. Therefore, it’s generally best to keep transactions as short as possible, and only use them when necessary for maintaining data consistency.

Closing the Connection

When you’re done with your database work, it’s essential to properly close all the resources you’ve opened: Connections, Statements, PreparedStatements, CallableStatements, and ResultSets. Failing to close these resources can lead to resource leaks, which can severely impact the performance and scalability of your application.

Think of it like borrowing books from a library. If you keep borrowing books without ever returning them, eventually the library will run out of books for other people to borrow. Similarly, if you keep opening database resources without closing them, eventually the database will run out of resources to allocate to other connections.

However, always keep in mind that the order matters. It’s not enough to just close the resources, you need to close them in the right order. The rule of thumb is to close the resources in the reverse order of their creation.

Here’s the typical order:

  1. Close the ResultSet (if there is one)
  2. Close the Statement, PreparedStatement, or CallableStatement
  3. Close the Connection

This order is important because of the way these resources are related:

Therefore:

Here’s an example that demonstrates the proper closing order:

ResultSet rs = null;
PreparedStatement pstmt = null;
Connection conn = null;

try {
    conn = DriverManager.getConnection(url, user, password);
    pstmt = conn.prepareStatement(sql);
    rs = pstmt.executeQuery();
    // process the results
} catch (SQLException e) {
    // handle the exception
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            // log the exception
        }
    }
    if (pstmt != null) {
        try {
            pstmt.close();
        } catch (SQLException e) {
            // log the exception  
        }
    }
    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException e) {
            // log the exception
        }
    }
}

Notice that we close the resources in the finally block. This ensures that they get closed even if an exception occurs. Note that each close() call is wrapped in its own try-catch block. This is because the close() method can also throw a SQLException, and we don’t want that to prevent the other resources from being closed. Also, the code checks if the resources are null first, to avoid a NullPointerException if the resources were never initialized.

However, you can use the try-with-resources statement to automatically close resources:

try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement pstmt = conn.prepareStatement(sql);
     ResultSet rs = pstmt.executeQuery()) {
    // process the results
} catch (SQLException e) {
    // handle the exception
}

The try-with-resources statement automatically closes the resources in the reverse order of their declaration, even if an exception occurs. This is the preferred way to handle JDBC resources because it greatly simplifies your code and make it more readable.

Key Points

Practice Questions

1. Which of the following statements correctly creates a database connection using JDBC?

A) Connection connection = DriverManager.connect("jdbc:mysql://localhost:3306/mydatabase");
B) Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user");
C) Connection connection = DriverManager.connect("jdbc:mysql://localhost:3306/mydatabase", "user", "password");
D) Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password");

2. Which of the following statements correctly executes a SQL query using a Statement object in JDBC?

A) ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
B) int result = statement.executeQuery("SELECT * FROM users");
C) ResultSet result = statement.execute("SELECT * FROM users");
D) int resultSet = statement.executeUpdate("SELECT * FROM users");

3. Which of the following statements correctly prepares and executes a parameterized SQL query using a PreparedStatement object in JDBC?

A)

PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
ResultSet rs = pstmt.executeQuery(1);

B)

PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
pstmt.setInt(1, 1);
ResultSet rs = pstmt.executeQuery();

C)

PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM users WHERE id = 1");
ResultSet rs = pstmt.execute();

D)

PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
pstmt.setInt(1, 1);
boolean rs = pstmt.execute();

4. Given a users table with the columns id (INT) and name (VARCHAR), which of the following statements correctly retrieves all the data from a ResultSet object in JDBC?

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class RetrieveData {
    public static void main(String[] args) {
        try {
            Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydatabase", 
                "user", "password"
            );
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(
                "SELECT id, name FROM users"
            );

            // Retrieving data
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

A)

while (resultSet.next()) {
    String id = resultSet.getString("id");
    int name = resultSet.getInt("name");
}

B)

if (resultSet.next()) {
    int id = resultSet.getInt(1);
    String name = resultSet.getString(2);
}

C)

int id = resultSet.getInt(1);
String name = resultSet.getString(2);

D)

while (resultSet.next()) {
    int id = resultSet.getInt("id");
    String name = resultSet.getString("name");
}

5. Which of the following statements correctly demonstrates how to manage transactions using JDBC?

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class TransactionExample {
    public static void main(String[] args) {
        try {
            Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydatabase", 
                "user", "password"
            );
            Statement statement = connection.createStatement();
            
            // Managing transaction
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

A)

connection.setAutoCommit(true);
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.commit();

B)

connection.setAutoCommit(false);
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.commit();

C)

connection.setAutoCommit(false);
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.rollback();

D)

connection.setTransaction(true);
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.commit();

6. Which of the following statements correctly demonstrates the use of savepoints in JDBC transactions?

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;

public class SavepointExample {
    public static void main(String[] args) {
        try {
            Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydatabase", 
                "user", "password"
            );
            Statement statement = connection.createStatement();
            connection.setAutoCommit(false);

            // Using savepoints
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

A)

Savepoint savepoint1 = connection.setSavepoint();
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.rollback(savepoint1);
connection.commit();

B)

Savepoint savepoint1 = connection.setSavepoint();
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.rollback();
statement.executeUpdate("INSERT INTO users (id, name) VALUES (3, 'Jane')");
connection.commit();

C)

Savepoint savepoint1 = connection.setSavepoint("Savepoint1");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.rollback(savepoint1);
statement.executeUpdate("INSERT INTO users (id, name) VALUES (3, 'Jane')");
connection.commit();

D)

Savepoint savepoint1 = connection.savepoint("Savepoint1");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'John')");
statement.executeUpdate("INSERT INTO users (id, name) VALUES (2, 'Doe')");
connection.rollbackToSavepoint(savepoint1);
statement.executeUpdate("INSERT INTO users (id, name) VALUES (3, 'Jane')");
connection.commit();

7. Which of the following statements correctly demonstrates how to close JDBC resources properly in a finally block?

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class CloseResourcesExample {
    public static void main(String[] args) {
        Connection connection = null;
        Statement statement = null;
        try {
            connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydatabase", 
                "user", "password"
            );
            statement = connection.createStatement();
            statement.executeUpdate(
                "INSERT INTO users (id, name) VALUES (1, 'John')"
            );
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // Closing resources
        }
    }
}

A)

finally {
    if (statement != null) {
        statement.close();
    }
    if (connection != null) {
        connection.close();
    }
}

B)

finally {
    try {
        statement.close();
        connection.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

C)

finally {
    try {
        if (statement != null) {
            statement.close();
        }
        if (connection != null) {
            connection.close();
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

D)

finally {
    statement.close();
    connection.close();
}

Do you like what you read? Would you consider?


Do you have a problem or something to say?

Report an issue with the book

Contact me