JDBC Best Practices

JDBC is Java's lowest-level database API. Higher-level tools (JPA, Spring Data) sit on top of it. Despite the prevalence of ORMs, raw JDBC remains useful for: performance-critical paths, complex queries the ORM doesn't express well, batch operations, and integration tests.

This page covers the patterns that make raw JDBC code sustainable.

Connection management

JDBC connections are expensive. Open, close per operation produces production-grade scalability problems quickly.

Always use a connection pool

Production code should use a connection pool (HikariCP is the standard; Spring Boot uses it by default). The pool keeps a fixed number of connections open and lends them out on request.

Direct JDBC code should never do `DriverManager.getConnection()` per request — that opens a new connection every call.

Try-with-resources

```java

try (Connection conn = dataSource.getConnection();

PreparedStatement stmt = conn.prepareStatement(sql);

ResultSet rs = stmt.executeQuery()) {

while (rs.next()) {

// process row

}

}

```

Resources close automatically. Replaces the verbose try-finally pattern. Always use this; never manually close JDBC resources.

Prepared statements

```java

// Wrong: SQL injection vulnerable

String sql = "SELECT * FROM orders WHERE customer_id = '" + customerId + "'";

// Right: parameterized

String sql = "SELECT * FROM orders WHERE customer_id = ?";

try (PreparedStatement stmt = conn.prepareStatement(sql)) {

stmt.setString(1, customerId);

try (ResultSet rs = stmt.executeQuery()) {

// ...

}

}

```

Parameterized queries:

- Prevent SQL injection

- Enable database query plan caching

- Faster on repeated execution

Always use prepared statements. Hand-built SQL strings are a vulnerability waiting to happen.

Batching

For bulk inserts or updates:

```java

String sql = "INSERT INTO orders (id, amount) VALUES (?, ?)";

try (PreparedStatement stmt = conn.prepareStatement(sql)) {

for (Order order : orders) {

stmt.setString(1, order.id());

stmt.setBigDecimal(2, order.amount());

stmt.addBatch();

}

stmt.executeBatch();

}

```

Dramatic speedup vs. individual INSERTs — often 10-100× for typical workloads. The exact speedup depends on the database and network.

For PostgreSQL specifically, additional batching options:

- `reWriteBatchedInserts=true` connection parameter

- `INSERT ... ON CONFLICT` with multi-row VALUES

Transactions

```java

conn.setAutoCommit(false);

try {

// multiple statements

insertOrder(conn, order);

insertOrderItems(conn, items);

conn.commit();

} catch (Exception e) {

conn.rollback();

throw e;

}

```

Transaction management at the JDBC level is verbose; framework-level transaction management (Spring's `@Transactional`) is usually preferred for application code.

For batch operations, transactions are essential — committing every row defeats batching.

Result set handling

```java

try (ResultSet rs = stmt.executeQuery()) {

while (rs.next()) {

String id = rs.getString("id");

BigDecimal amount = rs.getBigDecimal("amount");

OrderStatus status = OrderStatus.valueOf(rs.getString("status"));

// ...

}

}

```

Notes:

- Get by column name, not index, for clarity

- `rs.next()` returns false at end; the loop must handle empty results

- Type-safe getters (`getString`, `getInt`, `getBigDecimal`) match the database type

For null handling, primitives are tricky:

```java

int count = rs.getInt("count");

if (rs.wasNull()) {

// count was actually NULL in the database

}

```

Or use `getObject(...)` and check for null directly.

Mapping ResultSet to objects

Three approaches:

Manual

```java

private Order mapRow(ResultSet rs) throws SQLException {

return new Order(

rs.getString("id"),

rs.getBigDecimal("amount"),

OrderStatus.valueOf(rs.getString("status"))

);

}

```

Verbose but explicit. Errors are clear.

Spring's JdbcTemplate

```java

List<Order> orders = jdbcTemplate.query(sql, args, this::mapRow);

```

Reduces the connection management boilerplate. Still requires manual mapping.

Reflection-based libraries

JOOQ, jOOQ, MyBatis, etc. Generate or reflect-map; less code, more magic. Trade-offs depend on library.

Specific patterns

Pagination

```sql

SELECT * FROM orders

ORDER BY created_at, id

LIMIT 100 OFFSET 200

```

For most cases, OFFSET is fine. For deep pagination, cursor-based pagination is faster:

```sql

SELECT * FROM orders

WHERE (created_at, id) > (?, ?)

ORDER BY created_at, id

LIMIT 100

```

Streaming large result sets

For result sets too large to load into memory, use `setFetchSize` and stream:

```java

stmt.setFetchSize(1000);

try (ResultSet rs = stmt.executeQuery()) {

while (rs.next()) {

// process row, don't accumulate

}

}

```

The driver fetches rows in batches; memory stays bounded.

IN clauses with large lists

```java

// Bad: SQL with many parameters fails

String sql = "WHERE id IN (?, ?, ?, ..., ?)"; // up to 100s of placeholders

// Better: temp table or array parameter (database-specific)

```

PostgreSQL supports array parameters: `WHERE id = ANY(?::uuid[])`. Other databases vary.

Database time and locale

`PreparedStatement.setTimestamp()` and `ResultSet.getTimestamp()` interact with timezone conversion. Be deliberate about which timezone the database stores in (usually UTC) and how the application converts.

Common failure patterns

- **Not using prepared statements.** SQL injection, plus performance.

- **Connection leaks.** Connections not returned to pool. Try-with-resources prevents this.

- **N+1 queries.** A loop of `SELECT` calls is much slower than one batched query.

- **Catching SQLException without context.** The exception class is not very informative; wrap with context.

- **Bulk operations without batching.** Slow at scale.

- **Mixing transaction management styles.** JDBC-level and framework-level transactions can conflict.

Further Reading

- [JpaAndHibernatePatterns](JpaAndHibernatePatterns) — ORM that sits on top of JDBC

- [SpringBootFundamentals](SpringBootFundamentals) — Spring's data access conventions

- [DatabaseConnectionSecurity](DatabaseConnectionSecurity) — Connection security practices

- [Java Hub](JavaHub) — Cluster index