How do I execute a simple SQL query with Statement?

Executing a simple SQL query using a Statement object in JDBC follows a straightforward pattern: establish a connection, create the statement, execute the query, and process the results.

Here is a clean example of how to perform a SELECT query:

Simple SQL Query Example

package org.kodejava.jdbc;

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

public class SimpleQueryExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/your_database";
        String user = "username";
        String password = "password";

        String sql = "SELECT id, username, email FROM users";

        // Use try-with-resources to ensure resources are closed automatically
        try (Connection conn = DriverManager.getConnection(url, user, password);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            // Iterate through the result set
            while (rs.next()) {
                int id = rs.getInt("id");
                String name = rs.getString("username");
                String email = rs.getString("email");

                System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Key Methods to Know

The Statement interface provides different methods depending on the type of SQL you are running:

  1. executeQuery(String sql): Used for SELECT statements. It returns a ResultSet containing the data.
  2. executeUpdate(String sql): Used for INSERT, UPDATE, or DELETE statements. It returns an int representing the number of rows affected.
  3. execute(String sql): A general-purpose method that can execute any SQL statement. It returns true if the result is a ResultSet (query) and false if it is an update count or there are no results.

Important Tips

  • Try-with-Resources: Always use the try-with-resources block (shown above) for Connection, Statement, and ResultSet. This prevents memory leaks by ensuring the database handles are closed even if an exception occurs.
  • Security: While Statement is great for simple or static queries, use PreparedStatement if your query includes variables provided by a user. This prevents SQL Injection attacks.
  • Indices vs. Names: When reading from a ResultSet, you can use column names (e.g., rs.getString("username")) or 1-based indices (e.g., rs.getString(1)). Names are generally more readable and maintainable.

How do I close JDBC resources properly?

Properly closing JDBC resources is crucial to prevent memory leaks and database connection exhaustion. In modern Java, the absolute best way to do this is by using the try-with-resources statement.

The Best Practice: Try-with-Resources

Introduced in Java 7, this approach automatically closes any resource that implements java.lang.AutoCloseable (which Connection, Statement, and ResultSet all do) at the end of the block, even if an exception occurs.

You should declare your resources in the parentheses of the try block in the order they are created:

package org.kodejava.jdbc;

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

public class JdbcResourceManagement {
    private static final String URL = "jdbc:mysql://localhost:3306/your_database";
    private static final String USER = "user";
    private static final String PASSWORD = "password";

    public void fetchData() {
        String query = "SELECT id, name FROM users";

        // Resources are closed in reverse order of their creation
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(query)) {

            while (rs.next()) {
                System.out.println("ID: " + rs.getInt("id") + ", Name: " + rs.getString("name"));
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }
        // No finally block needed for closing!
    }
}

Why this is the “Proper” Way:

  1. Reverse Closure: Resources are closed in the reverse order of their initialization (ResultSet → Statement → Connection).
  2. Exception Safety: If an exception occurs while closing a resource, it doesn’t prevent the others from closing. If multiple exceptions occur, the primary exception is thrown, and the “close” exceptions are attached as “suppressed” exceptions.
  3. Readability: It eliminates the “pyramid of doom” found in older finally blocks where you had to wrap every .close() call in its own try-catch to handle potential nulls and nested SQLExceptions.

Important Note on Connection Pools

If you are using a DataSource (like HikariCP or Apache Commons DBCP), calling conn.close() inside a try-with-resources block does not actually shut down the physical connection to the database. Instead, it “returns” the connection to the pool so it can be reused by another part of your application. This is why closing resources is just as important when using pools—forgetting to “close” a pooled connection will eventually lead to a “Pool Exhausted” error.

How do I use DataSource instead of DriverManager?

Switching from DriverManager to DataSource is a best practice in modern Java applications because it supports connection pooling, is more configurable, and decouples your code from the specific database driver implementation.

While DriverManager creates a physical connection every time you call getConnection(), a DataSource (specifically a pooling one) maintains a set of open connections that can be reused, significantly improving performance.

1. The DriverManager Approach (Old way)

You are likely used to this pattern:

// Hardcoded driver details and physical connection creation
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db", "user", "pass");

2. The DataSource Approach (Modern way)

With DataSource, you configure the object once and then use it to get connections throughout your application.

Using Apache Commons DBCP (Connection Pooling)

To use a DataSource with pooling, you can use a library like Apache Commons DBCP’s BasicDataSource.

package org.kodejava.jdbc;

import org.apache.commons.dbcp2.BasicDataSource;
import javax.sql.DataSource;

public class DatabaseConfig {
    private static final BasicDataSource dataSource;

    static {
        dataSource = new BasicDataSource();
        dataSource.setUrl("jdbc:mysql://localhost/musicdb");
        dataSource.setUsername("music");
        dataSource.setPassword("s3cr*t");

        // Optional: Configure pooling parameters
        dataSource.setInitialSize(5);
        dataSource.setMaxTotal(10);
    }

    public static DataSource getDataSource() {
        return dataSource;
    }
}

3. Using the Connection in your Code

Once you have the DataSource instance, getting a connection is consistent regardless of the underlying implementation:

public void fetchData() {
    DataSource ds = DatabaseConfig.getDataSource();

    // The try-with-resources ensures the connection is "closed" 
    // (returned to the pool) automatically.
    try (Connection conn = ds.getConnection()) {
        // Use the connection as usual
        var stmt = conn.prepareStatement("SELECT * FROM record");
        var rs = stmt.executeQuery();
        // ... process results
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

Why use DataSource?

  • Connection Pooling: Reusing connections is much faster than opening/closing them for every request.
  • Decoupling: Your business logic only knows about the javax.sql.DataSource interface. You can switch from BasicDataSource to HikariCP (another popular pool) without changing your data-access code.
  • JNDI Support: In Jakarta EE environments, you can look up a DataSource configured in the application server via JNDI, keeping credentials out of your source code.
  • Spring Integration: If you use Spring Framework, JdbcTemplate is designed to work directly with a DataSource.

Maven Dependency

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.13.0</version>
</dependency>

How do I create a database connection with DriverManager?

Creating a database connection with DriverManager is the standard way to establish a session with a database in JDBC.

1. The Essential Formula

To get a connection, you call DriverManager.getConnection() using a Connection URL, a username, and a password.

Connection connection = DriverManager.getConnection(url, username, password);

2. Implementation Example

In modern Java (JDBC 4.0+), you don’t need to manually load the driver class with Class.forName(). The DriverManager will automatically find the driver on your classpath.

It is best practice to use a try-with-resources block to ensure the connection is closed automatically, even if an error occurs.

package org.kodejava.jdbc;

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

public class DatabaseConnection {
    public static void main(String[] args) {
        // 1. Define connection parameters
        String url = "jdbc:mysql://localhost:3306/your_database";
        String user = "your_username";
        String password = "your_password";

        // 2. Establish connection within try-with-resources
        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            if (conn != null) {
                System.out.println("Successfully connected to the database!");
            }
        } catch (SQLException e) {
            System.err.println("Connection failed!");
            e.printStackTrace();
        }
    }
}

3. Common Connection URLs

The format of the URL varies depending on the database you are using:

  • MySQL: jdbc:mysql://localhost:3306/db_name
  • PostgreSQL: jdbc:postgresql://localhost:5432/db_name
  • Oracle: jdbc:oracle:thin:@localhost:1521:xe
  • SQL Server: jdbc:sqlserver://localhost:1433;databaseName=db_name

Key Points to Remember:

  • Classpath: Ensure the database driver JAR (like mysql-connector-j or postgresql) is in your project’s dependencies.
  • Exception Handling: Database operations always throw a SQLException, so they must be inside a try-catch block.
  • Security Tip: Avoid hardcoding passwords in your source code. Use environment variables or configuration files instead.

How do I load a JDBC driver in modern Java?

In modern Java (specifically JDBC 4.0 and later), you generally do not need to write code to load the driver.

The DriverManager uses the Service Provider Interface (SPI) to automatically discover and load any JDBC drivers present on your classpath.

1. The Modern Way: Automatic Discovery

As long as the driver JAR is on your classpath (e.g., added via Maven or Gradle), you can simply request a connection:

package org.kodejava.jdbc;

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

public class DatabaseApp {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/my_database";
        String user = "username";
        String password = "password";

        // No Class.forName() needed!
        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            System.out.println("Connected successfully!");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

2. The Legacy Way (Avoid if possible)

In older versions of Java (pre-Java 6) or specific legacy environments, you might see this line:

Class.forName("com.mysql.cj.jdbc.Driver");

While this still works, it is considered boilerplate in modern applications because the DriverManager handles this initialization automatically during the first call to getConnection().

3. How to ensure it works

The “loading” now happens at the project configuration level rather than the code level:

  • Maven: Add the dependency to your pom.xml.
  • Gradle: Add the implementation to your build.gradle.
  • Plain JARs: Ensure the driver .jar file is included in your IDE’s libraries or the -classpath argument when running the app.

Why did this change?

The JDBC 4.0 specification introduced the META-INF/services/java.sql.Driver file inside driver JARs. When you call DriverManager.getConnection(), Java scans the classpath for these files and registers any drivers it finds automatically.