JDBC: connecting to MySQL
Connector/J, DriverManager, PreparedStatement, ResultSet, CRUD, transactions and SQL-injection safety.
Almost every business application stores its data in a relational database. JDBC (Java Database Connectivity, packages java.sql and javax.sql) is the standard API for talking to databases from Java. Frameworks like Spring Data JPA and Hibernate are built on top of it, so understanding JDBC makes you better at all of them. In this lesson we connect to MySQL 8, run queries safely, and use transactions.
Prerequisite: a running MySQL 8.x server. The MySQL course on Elephantoo shows how to install one. On Ubuntu/Debian,
sudo apt install mysql-server(ormariadb-server, which works with the same driver for these examples); on Fedora,sudo dnf install community-mysql-server; or rundocker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:8.4.
How JDBC fits together#
You program against the standard interfaces; the driver JAR translates to the MySQL protocol. Switching to PostgreSQL mostly means swapping the driver and the connection URL.
Step 1: create a database and user#
In the mysql client (as root):
Give applications their own user with access only to their own database, never root.
Step 2: add the MySQL driver#
With Maven, add the dependency (see the build tools lesson):
Or with Gradle: runtimeOnly("com.mysql:mysql-connector-j:26.7.0"). Any recent Connector/J version (9.x or newer) works with MySQL 8.x. For quick single-file experiments, download the JAR from Maven Central and put it on the classpath:
Modern drivers register themselves automatically; the old Class.forName("com.mysql.cj.jdbc.Driver") line is no longer needed.
Step 3: connect#
A connection URL has the form jdbc:mysql://host:port/database?options:
ConnectionisAutoCloseable. Always open it in try-with-resources; leaked connections exhaust the server.- Read credentials from environment variables or configuration, never hard-code real passwords in source code.
- If it fails, the message usually tells you why:
Access denied for user(credentials),Communications link failure(server not running or wrong host/port),Unknown database(typo or missingCREATE DATABASE).
Step 4: create a table and run statements#
Statement runs fixed SQL. For DDL without user input it's fine:
PreparedStatement: parameters done right#
Whenever a value comes from outside (users, files, APIs), use a PreparedStatement with ? placeholders:
This DAO (Data Access Object) pattern keeps SQL in one class and exposes plain Java methods to the rest of the application.
Reading a ResultSet
- A
ResultSetis a cursor positioned before the first row.rs.next()moves to the next row and returnsfalsewhen there are no more. - Read columns by label (
rs.getString("name")) or 1-based index (rs.getString(1)). Labels are clearer. - Typed getters:
getInt,getLong,getString,getBigDecimal,getBoolean,getObject(col, LocalDate.class)... - For nullable numeric columns,
getIntreturns0for SQLNULL. Checkrs.wasNull()afterwards, or users.getObject("col", Integer.class), which returnsnull.
Java ↔ MySQL types
SQL injection: why concatenation is dangerous#
The attacker's input changed the meaning of the query: OR '1'='1' is always true, so it returns every user. Worse inputs can delete tables or read passwords. With PreparedStatement, the same input is just an odd-looking name that matches nothing:
Rule: never build SQL by concatenating values. Placeholders can't be used for table or column names; if those must vary, choose them from a fixed allow-list in code.
Transactions#
By default each statement is committed immediately (auto-commit). A transaction groups statements so they succeed or fail together. The classic example is a money transfer:
The failed transfer had already debited Asha when the credit failed; rollback() undid it. Transactions require a transactional storage engine, which MySQL's default InnoDB is.
Batch inserts#
Sending rows one at a time costs a network round trip each. Batching is far faster for bulk loads:
Add ?rewriteBatchedStatements=true to the MySQL URL so Connector/J combines the batch into multi-row INSERTs.
Connection pools#
Opening a database connection takes time (network handshake, authentication). Real applications use a connection pool that keeps connections open and lends them out. HikariCP is the standard (and Spring Boot's default):
Code written against DataSource works the same with or without a pool.
Beyond plain JDBC#
Raw JDBC is verbose. In practice you'll often use:
- Spring's
JdbcClient/JdbcTemplate: JDBC without the boilerplate. - JPA/Hibernate with Spring Data JPA: map classes to tables and write
interface ProductRepository extends JpaRepository<Product, Long>. - jOOQ: type-safe SQL built in Java code.
- Flyway or Liquibase: versioned schema migrations.
All of them use JDBC underneath, and every concept here (connections, prepared statements, transactions, pools) still applies.
Common mistakes#
- Concatenating user input into SQL.
- Not closing
Connection,PreparedStatementandResultSet. Use try-with-resources for all three. - Using
doublefor money columns instead ofDECIMAL+BigDecimal. - Forgetting
rs.next()before reading the first row (Illegal operation on empty result set). - 0-based indexes: JDBC parameters and columns start at 1.
- Committing after a partial failure instead of rolling back.
- Hard-coding passwords in source control.
What's next#
Juggling JARs and classpaths by hand, as we did with the driver, doesn't scale. Next, Maven and Gradle manage dependencies, builds and tests for you.
Check your understanding
Quick quiz
1.Why should you use
PreparedStatementwith?placeholders instead of concatenating user input into SQL?2.What is the index of the first column in
rs.getString(1)?3.You call
conn.setAutoCommit(false), run two UPDATEs, and the second throws an exception. What should your code do?
Finished reading?
Mark this lesson complete to track your progress.