JDBC Fundamentals in Java
JDBC is the standard API for talking to a relational database from Java: a connection, a statement, and a result set.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
What JDBC is
JDBC is a set of interfaces in the standard library. Each database vendor supplies a driver that implements them, so the same code works against different databases with only the connection details changing.
Your code -> JDBC API -> Driver -> DatabaseThe four steps
String url = "jdbc:mysql://localhost:3306/notes_management";
try (Connection connection = DriverManager.getConnection(url, user, password);
PreparedStatement statement = connection.prepareStatement(
"SELECT id, title, views_count FROM notes WHERE status = ?")) {
statement.setString(1, "published");
try (ResultSet rows = statement.executeQuery()) {
while (rows.next()) {
long id = rows.getLong("id");
String title = rows.getString("title");
int views = rows.getInt("views_count");
System.out.printf("%d %s (%d)%n", id, title, views);
}
}
}- Open a
Connection. - Create a
Statement, almost always aPreparedStatement. - Execute it and read the
ResultSet. - Close everything, which try with resources does for you.
Since JDBC 4 the driver is discovered automatically from the classpath. Class.forName("com.mysql.jdbc.Driver") appears in older tutorials and is no longer needed.The connection URL
jdbc:mysql://host:3306/database?useSSL=true
jdbc:postgresql://host:5432/database
jdbc:h2:mem:testdb an in memory database, useful in tests
jdbc:sqlite:notes.dbThe three statement types
| Type | Use for | Parameters |
|---|---|---|
Statement | Static SQL with no input | None; concatenation is unsafe |
PreparedStatement | Almost everything | Bound with ? placeholders |
CallableStatement | Stored procedures | Input and output parameters |
Reading a ResultSet
try (ResultSet rows = statement.executeQuery()) {
while (rows.next()) { // starts before the first row
String title = rows.getString("title");
Integer views = rows.getObject("views_count", Integer.class); // null safe
LocalDate published = rows.getObject("published_on", LocalDate.class);
int raw = rows.getInt("views_count");
if (rows.wasNull()) { // getInt returns 0 for SQL NULL
raw = -1;
}
}
}The primitive getters cannot representNULL:getIntreturns 0 andgetBooleanreturnsfalse. Either callwasNull()immediately afterwards, or usegetObjectwith a wrapper type, which returnsnullhonestly. This is one of the most common JDBC bugs.
java.time types
statement.setObject(1, LocalDate.of(2026, 8, 21));
statement.setObject(2, Instant.now().atOffset(ZoneOffset.UTC));
LocalDate date = rows.getObject("published_on", LocalDate.class);
OffsetDateTime created = rows.getObject("created_at", OffsetDateTime.class);JDBC 4.2 maps java.time types directly. java.sql.Date and java.sql.Timestamp are legacy and should not appear in new code.
Insert, update and delete
String sql = "INSERT INTO notes (title, slug, status) VALUES (?, ?, ?)";
try (PreparedStatement statement =
connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, "JDBC basics");
statement.setString(2, "jdbc-basics");
statement.setString(3, "draft");
int affected = statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) {
long id = keys.getLong(1);
System.out.println("Inserted note " + id);
}
}
}Mapping rows to objects
public record Note(long id, String title, int views, LocalDate published) { }
public List<Note> findPublished() throws SQLException {
String sql = """
SELECT id, title, views_count, published_on
FROM notes
WHERE status = ?
ORDER BY published_on DESC""";
List<Note> notes = new ArrayList<>();
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "published");
try (ResultSet rows = statement.executeQuery()) {
while (rows.next()) {
notes.add(new Note(
rows.getLong("id"),
rows.getString("title"),
rows.getInt("views_count"),
rows.getObject("published_on", LocalDate.class)));
}
}
}
return notes;
}DataSource and pooling
// DriverManager opens a fresh connection every time: slow
Connection connection = DriverManager.getConnection(url, user, password);
// A pooled DataSource reuses connections
DataSource dataSource = configuredPool();
try (Connection pooled = dataSource.getConnection()) {
// close() returns it to the pool rather than closing the socket
}Opening a database connection involves a network round trip and authentication, so it is expensive. Any real application uses a connection pool behind a DataSource. Keep the mention brief: the pool is a library, the DataSource interface is standard.
Closing matters
// Leaks a connection on any exception
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet rows = statement.executeQuery(sql);
// ...
rows.close();
statement.close();
connection.close();
// Closed on every path, in reverse order
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet rows = statement.executeQuery()) {
// ...
}A leaked connection is not reclaimed by garbage collection in any timely way. Enough of them and the pool is exhausted and the application stops.
Metadata
DatabaseMetaData metadata = connection.getMetaData();
System.out.println(metadata.getDatabaseProductName());
System.out.println(metadata.getDriverVersion());
ResultSetMetaData columns = rows.getMetaData();
for (int i = 1; i <= columns.getColumnCount(); i++) {
System.out.println(columns.getColumnLabel(i) + " " + columns.getColumnTypeName(i));
}Note that JDBC column indexes start at 1, not 0. This catches almost everyone at least once.
Exceptions
try {
repository.save(note);
} catch (SQLException e) {
logger.severe("SQL state " + e.getSQLState() + ", code " + e.getErrorCode());
throw new StorageException("Could not save note", e);
}SQLException is checked. getSQLState() returns a standard code and getErrorCode() a vendor specific one, which is how a duplicate key can be distinguished from a connection failure.
Common mistakes
- Building SQL by concatenating input, which opens an injection hole.
- Not closing connections, statements or result sets.
- Using index 0 for a column or a parameter.
- Ignoring
wasNull()after a primitive getter. - Calling
DriverManagerper request instead of using a pool. - Selecting every column when only two are needed.
- Loading a whole table into memory instead of filtering in SQL.
Best practices
- Always use
PreparedStatementwith bound parameters. - Always use try with resources.
- Use a pooled
DataSource. - Select only the columns you use.
- Use
getObjectwith a wrapper orjava.timetype for nullable columns. - Wrap
SQLExceptionin a domain exception, keeping the cause. - Keep SQL in one layer rather than scattered through the application.
Practice
- Write a method that returns every note in a category, using a bound parameter.
- Explain why
getIntreturns 0 for a NULL column and how to detect it. - Insert a row and read back the generated key.
- Why does a leaked connection eventually stop the whole application?
- Convert a manual close sequence into try with resources and list what improved.
Conclusion
JDBC is a small API: a connection, a prepared statement, a result set. Bind parameters, close everything with try with resources, pool your connections, and map rows into records at the boundary.