"Database connectivity lets your Java programs store, retrieve, and manipulate persistent data managed by a relational database — far more powerful and scalable than flat file storage."- Claude 2026
In another page we used file I/O to persist data. Files work for small, simple datasets, but as data grows — multiple tables, relationships between records, concurrent users — a relational database management system (RDBMS) becomes essential.
Data stored in flat text files. Simple to set up, but no structure enforcement, no querying power, and no support for multiple users accessing data at the same time.
Data stored in structured tables with defined columns and types. Supports complex queries, relationships between tables, data validation, and concurrent access by many users.
Java connects to databases through JDBC (Java Database Connectivity), an API included in the standard library. JDBC provides a uniform interface — the same Java code structure works whether you're talking to MySQL, PostgreSQL, SQLite, or any other RDBMS. The only things that change are the driver (a JAR file specific to your database) and the connection URL.
Before writing Java code, you need to understand the SQL statements your program will send to the database. SQL breaks down into two main categories:
Creates and modifies the structure of your database.
CREATE TABLE characters ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(50) NOT NULL, char_class VARCHAR(50) NOT NULL ); ALTER TABLE characters ADD level INT; DROP TABLE characters;
Works with the data inside your tables.
INSERT INTO characters (name, char_class) VALUES ('Hero', 'Warrior'); SELECT * FROM characters; UPDATE characters SET char_class = 'Paladin' WHERE name = 'Hero'; DELETE FROM characters WHERE name = 'Hero';
Each of these statements will be sent from your Java program to the database through a JDBC connection. The database executes the SQL and returns results (or a confirmation) back to your program.
You can try out SQL statements right now without installing anything. SQLize.online gives you a live MySQL database in the browser — no login required. Paste any of the SQL examples from this page, click Run, and see the results.
JDBC uses three key interfaces to send SQL to the database and handle what comes back. Think of them as a pipeline:
Your active session with the database. You obtain one by calling DriverManager.getConnection() with a connection URL (and for MySQL, a username and password). Every database operation flows through this connection.
DriverManager.getConnection()
A precompiled SQL statement created from your Connection using connection.prepareStatement(sql). Placeholders written as ? in the SQL are filled in with setter methods like setString(1, value) before execution.
connection.prepareStatement(sql)
?
setString(1, value)
Always use PreparedStatement instead of building SQL strings with concatenation. Concatenating user input into SQL opens the door to SQL injection — a serious security vulnerability where malicious input gets executed as SQL code.
Holds the rows returned by a SELECT query. You iterate through it with resultSet.next(), which advances to the next row and returns false when there are no more rows. Column values are retrieved with typed getters like getString("name") or getInt("id").
SELECT
resultSet.next()
false
getString("name")
getInt("id")
The execution method you call depends on the type of SQL:
executeQuery()
For SELECT statements.Returns a ResultSet.
executeUpdate()
For INSERT, UPDATE, DELETE, and DDL.Returns an int (rows affected).
INSERT
UPDATE
DELETE
A typical JDBC workflow follows a predictable pattern:
DriverManager.getConnection(url)
pstmt.setString(1, "Hero")
while (rs.next()) { ... }
try (...) { }
Inserting data uses executeUpdate() with an INSERT statement. Each ? placeholder is filled using the appropriate setter. Positions are 1-indexed — the first ? is position 1, the second is position 2.
Querying data uses executeQuery() with a SELECT statement. The returned ResultSet is processed row by row inside a while loop.
while
Updating and deleting also use executeUpdate(). The return value tells you how many rows were affected, which is useful for confirming the operation worked.
File operations in Java use try-with-resources to automatically close readers and writers. JDBC works the same way — wrap your Connection, PreparedStatement, and ResultSet in a try (...) { } block, and Java closes them automatically when the block exits, even if an exception occurs.
The JDBC URL tells Java where your database lives. The format varies by database:
SQLite: jdbc:sqlite:game.db MySQL: jdbc:mysql://localhost:3306/game
SQLite creates a file (game.db) in the same folder as your program. MySQL connects to a running server on port 3306.
game.db
Each database needs its own driver — a JAR file you add to your project's classpath when you run the program.
For SQLite, the driver is sqlite-jdbc. For MySQL, it's MySQL Connector/J. In modern Java (6+), the driver registers itself automatically — no Class.forName() call is needed.
Class.forName()
The primary exception is SQLException, which covers connection failures, SQL syntax errors, and constraint violations. Always wrap JDBC operations in try-catch blocks.
SQLException
The try-with-resources syntax handles closing automatically — exactly like file I/O.
Never concatenate user input directly into SQL strings — always use PreparedStatement with ? placeholders.
Never hard-code database credentials in source code for production applications. Use environment variables or configuration files instead.
This complete example demonstrates every core JDBC operation: creating a table, inserting rows, querying, updating, and deleting. It uses SQLite, which stores the entire database in a single file — no server installation, no username/password, no background services to manage.
import java.sql.*; public class PartyDatabase { public static void main(String[] args) { String url = "jdbc:sqlite:game.db"; try (Connection conn = DriverManager.getConnection(url)) { Statement stmt = conn.createStatement(); stmt.executeUpdate( "CREATE TABLE IF NOT EXISTS characters (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "name VARCHAR(50) NOT NULL, " + "char_class VARCHAR(50) NOT NULL)" ); System.out.println("Table ready."); String insertSQL = "INSERT INTO characters (name, char_class) VALUES (?, ?)"; try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) { String[][] party = {{"Hero", "Warrior"}, {"Zara", "Mage"}, {"Fletch", "Archer"}}; for (String[] c : party) { pstmt.setString(1, c[0]); pstmt.setString(2, c[1]); pstmt.executeUpdate(); } } System.out.println("Characters inserted.\n"); String selectSQL = "SELECT id, name, char_class FROM characters"; try (PreparedStatement pstmt = conn.prepareStatement(selectSQL); ResultSet rs = pstmt.executeQuery()) { System.out.println("Current party:"); while (rs.next()) { System.out.printf(" [%d] %s (%s)%n", rs.getInt("id"), rs.getString("name"), rs.getString("char_class")); } } String updateSQL = "UPDATE characters SET char_class = ? WHERE name = ?"; try (PreparedStatement pstmt = conn.prepareStatement(updateSQL)) { pstmt.setString(1, "Paladin"); pstmt.setString(2, "Hero"); int rows = pstmt.executeUpdate(); System.out.println("\nUpdated " + rows + " row(s): Hero is now a Paladin."); } String deleteSQL = "DELETE FROM characters WHERE name = ?"; try (PreparedStatement pstmt = conn.prepareStatement(deleteSQL)) { pstmt.setString(1, "Fletch"); int rows = pstmt.executeUpdate(); System.out.println("Deleted " + rows + " row(s): Fletch removed.\n"); } try (PreparedStatement pstmt = conn.prepareStatement(selectSQL); ResultSet rs = pstmt.executeQuery()) { System.out.println("Updated party:"); while (rs.next()) { System.out.printf(" [%d] %s (%s)%n", rs.getInt("id"), rs.getString("name"), rs.getString("char_class")); } } } catch (SQLException e) { System.out.println("Database error: " + e.getMessage()); } } }
Table ready. Characters inserted. Current party: [1] Hero (Warrior) [2] Zara (Mage) [3] Fletch (Archer) Updated 1 row(s): Hero is now a Paladin. Deleted 1 row(s): Fletch removed. Updated party: [1] Hero (Paladin) [2] Zara (Mage)
This example uses SQLite, which requires no database server — just a single JAR file. You need two things: the JDK and the SQLite JDBC driver.
Go to sqlite-jdbc on GitHub. Under the latest release, expand the Assets section and download the file named sqlite-jdbc-<version>.jar (for example, sqlite-jdbc-3.51.3.0.jar). Save this JAR file in the same folder where you will save your .java file.
sqlite-jdbc-<version>.jar
sqlite-jdbc-3.51.3.0.jar
.java
Save the code above as PartyDatabase.java in the same folder as the JAR. Open a terminal in that folder and run:
PartyDatabase.java
javac PartyDatabase.java java -cp .:sqlite-jdbc-3.51.3.0.jar PartyDatabase
On Windows, replace the : with a ; in the classpath:
:
;
java -cp .;sqlite-jdbc-3.51.3.0.jar PartyDatabase
Adjust the JAR filename to match the version you downloaded. The -cp flag tells Java where to find both your compiled class (. means the current folder) and the SQLite driver.
-cp
.
The first time you run the program, SQLite creates a file called game.db in the same folder. This file is the database. You can delete it and run the program again to start fresh. If you run the program a second time without deleting game.db, the characters from the first run are still in the table — that's persistence at work.
ClassNotFoundException: the JAR file is not on your classpath. Make sure the -cp path includes the exact JAR filename and that the JAR is in the same directory as your compiled .class file.
.class
"No suitable driver found": same cause — the JAR isn't being found. Double-check the filename and path separator (: on Mac/Linux, ; on Windows).
Duplicate entries on re-run: the database file persists between runs. Delete game.db before running again, or change CREATE TABLE to CREATE TABLE IF NOT EXISTS and add logic to clear old data.
CREATE TABLE
CREATE TABLE IF NOT EXISTS
The example above uses SQLite to keep setup simple. When you're ready to work with MySQL — a full client-server database used in production applications — the JDBC code stays almost entirely the same. Only three things change:
jdbc:sqlite:game.db
jdbc:mysql://localhost:3306/game
getConnection(url)
getConnection(url, "root", "password")
AUTOINCREMENT
AUTO_INCREMENT
Everything else — the PreparedStatements, the ResultSet processing, the try-with-resources pattern, the parameterized queries — is identical. That's the power of JDBC's database-independent design: learn it once and it works with any database.
MySQL is a client-server database, which means you need to install and run a database server on your machine before your Java program can connect to it. This involves installing MySQL, setting a root password, creating a database, and downloading the MySQL JDBC driver (Connector/J). The following tutorials walk through every step with screenshots:
Install MySQL on Mac (CodeStudy)
Covers both Homebrew and the DMG installer, verifying installation, and logging in for the first time.
Install MySQL on Windows (Dataquest)
Step-by-step using the MySQL Installer with screenshots for every screen in the wizard.
Once MySQL is running, open a terminal (Mac) or the MySQL Command Line Client (Windows), log in, and create the database:
mysql -u root -p CREATE DATABASE game; exit;
Then download MySQL Connector/J (choose "Platform Independent", download the ZIP, and grab the .jar file inside). Place it in the same folder as your .java file and compile/run the same way:
.jar
javac PartyDatabase.java java -cp .:mysql-connector-j-9.2.0.jar PartyDatabase