1. Overview
In this article, we’re going to take a look at JDBC (Java Database Connectivity) which is an API for connecting and executing queries on a database.
JDBC can work with any database as long as proper drivers are provided.
2. JDBC Drivers
A JDBC driver is a JDBC API implementation used for connecting to a particular type of database. There are several types of JDBC drivers:
- Type 1 – contains a mapping to another data access API; an example of this is the JDBC-ODBC driver
- Type 2 – is an implementation that uses client-side libraries of the target database; also called a native-API driver
- Type 3 – uses middleware to convert JDBC calls into database-specific calls; also known as network protocol driver
- Type 4 – connect directly to a database by converting JDBC calls into database-specific calls; known as database protocol drivers or thin drivers,
The most commonly used type is type 4, as it has the advantage of being platform independent. Connecting directly to a database server provides better performance compared to other types. The downside of this type of driver is that it’s database-specific – given each database has its own specific protocol.
3. Connecting to a Database
To connect to a database, we simply have to initialize the driver and open a database connection.
3.1. Registering the Driver
For our example, we will use a type 4 database protocol driver.
Since we’re using a MySQL database, we need the mysql-connector-java dependency:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.6</version> </dependency>
Next, let’s register the driver using the Class.forName() method, which dynamically loads the driver class:
Class.forName("com.mysql.cj.jdbc.Driver");
3.2. Creating the Connection
To open a connection, we can use the getConnection() method of DriverManager class. This method requires a connection URL String parameter:
Connection con = DriverManager .getConnection("jdbc:mysql://localhost:3306/myDb", "user1", "pass");
The syntax of the connection URL depends on the type of database used. Let’s take a look at a few examples:
jdbc:mysql://localhost:3306/myDb?user=user1&password=pass
jdbc:postgresql://localhost/myDb
jdbc:hsqldb:mem:myDb
To connect to the specified myDb database, we will have to create the database and a user, and add grant a necessary access:
CREATE DATABASE myDb; CREATE USER 'user1' IDENTIFIED BY 'pass'; GRANT ALL on myDb.* TO 'user1';
4. Executing SQL Statements
The send SQL instructions to the database, we can use instances of type Statement, PreparedStatement or CallableStatement. These are obtained using the Connection object.
4.1. Statement
The Statement interface contains the essential functions for executing SQL commands.
First, let’s create a Statement object:
Statement stmt = con.createStatement();
Executing SQL instructions can be done through the use of three methods:
- executeQuery() for SELECT instructions
- executeUpdate() for updating the data or the database structure
- execute() can be used for both cases above when the result is unknown
Let’s use the execute() method to add a students table to our database:
String tableSql = "CREATE TABLE IF NOT EXISTS employees" + "(emp_id int PRIMARY KEY AUTO_INCREMENT, name varchar(30)," + "position varchar(30), salary double)"; stmt.execute(tableSql);
If the execute() method is used to update the data, then the stmt.getUpdateCount() method returns the number of rows affected.
If the result is 0 then either no rows were affected, or it was a database structure update command.
If the value is -1, then command was a SELECT query. The result can then be obtained using stmt.getResultSet().
Next, let’s add a record to our table using the executeUpdate() method:
String insertSql = "INSERT INTO employees(name, position, salary)" + " VALUES('john', 'developer', 2000)"; stmt.executeUpdate(insertSql);
The method returns the number of affected rows for a command that updates rows or 0 for a command that updates the database structure.
We can retrieve the records from the table using the executeQuery() method which returns an object of type ResultSet:
String selectSql = "SELECT * FROM employees"; ResultSet resultSet = stmt.executeQuery(selectSql);
When no longer using the Statement object, it’s important to release resources by closing them. This can be done either by using the Statement in a try-with-resources block, or by calling the close() method of Statement class by hand.
4.2. PreparedStatement
PreparedStatement objects contain precompiled SQL sequences. They can have one or more parameters denoted by a question mark.
Let’s create a PreparedStatement which updates records in the employees table based on given parameters:
String updatePositionSql = "UPDATE employees SET position=? WHERE emp_id=?"; PreparedStatement pstmt = con.prepareStatement(updatePositionSql);
To add parameters to the PreparedStatement, we can use simple setters – setX() – where X is the type of the parameter, and the method arguments are the order and value of the parameter:
pstmt.setString(1, "lead developer"); pstmt.setInt(2, 1);
The statement is executed with one of the same three methods described before: executeQuery(), executeUpdate(), execute() without the SQL String parameter:
int rowsAffected = pstmt.executeUpdate();
If you are retrieving user input to form SQL statements, it’s very important to use the PreparedStatement parameters for the values, and not concatenate them. This makes it possible to avoid SQL injection attacks.
4.3. CallableStatement
The CallableStatement interface allows calling stored procedures.
To create a CallableStatement object, we can use the prepareCall() method of Connection:
String preparedSql = "{call insertEmployee(?,?,?,?)}"; CallableStatement cstmt = con.prepareCall(preparedSql);
Setting input parameter values for the stored procedure is done like in the PreparedStatement interface, using setX() methods:
cstmt.setString(2, "ana"); cstmt.setString(3, "tester"); cstmt.setDouble(4, 2000);
If the stored procedure has output parameters, we need to add them using the registerOutParameter() method:
cstmt.registerOutParameter(1, Types.INTEGER);
Then let’s execute the statement and retrieve the returned value using a corresponding getX() method:
cstmt.execute(); int new_id = cstmt.getInt(1);
For example to work, we need to create the stored procedure in our MySql database:
delimiter // CREATE PROCEDURE insertEmployee(OUT emp_id int, IN emp_name varchar(30), IN position varchar(30), IN salary double) BEGIN INSERT INTO employees(name, position,salary) VALUES (emp_name,position,salary); SET emp_id = LAST_INSERT_ID(); END // delimiter ;
The insertEmployee procedure above will insert a new record into the employees table using the given parameters and return the id of the new record in the emp_id out parameter.
To be able to run a stored procedure from Java, the connection user needs to have access to the stored procedure’s metadata. This can be achieved by granting rights to the user on all stored procedures in all databases:
GRANT ALL ON mysql.proc TO 'user1';
Alternatively, we can open the connection with the property noAccessToProcedureBodies set to true:
con = DriverManager.getConnection( "jdbc:mysql://localhost:3306/myDb?noAccessToProcedureBodies=true", "user1", "pass");
This will inform the JDBC API that the user does not have rights to read the procedure metadata so that it will create all parameters as INOUT String parameters.
Both PreparedStatement and CallableStatement extend the Statement interface and can be closed in a similar manner.
5. Parsing Query Results
After executing a query, the result is represented by a ResultSet object, with has a structure similar to a table, with lines and columns.
5.1. ResultSet Interface
The ResultSet uses the next() method to move to the next line.
Let’s first create an Employee class to store our retrieved records:
public class Employee { private int id; private String name; private String position; private double salary; // standard constructor, getters, setters }
Next, let’s traverse the ResultSet and create an Employee object for each record:
String selectSql = "SELECT * FROM employees"; ResultSet resultSet = stmt.executeQuery(selectSql); List<Employee> employees = new ArrayList<>(); while (resultSet.next()) { Employee emp = new Employee(); emp.setId(resultSet.getInt("emp_id")); emp.setName(resultSet.getString("name")); emp.setPosition(resultSet.getString("position")); emp.setSalary(resultSet.getDouble("salary")); employees.add(emp); }
Retrieving the value for each table cell can be done using methods of type getX() where X represents the type of the cell data.
The getX() methods can be used with an int parameter representing the order of the cell, or a String parameter representing the name of the column. The latter option is preferable in case we change the order of the columns in the query.
5.2. Updatable ResultSet
Implicitly, a ResultSet object can only be traversed forward and cannot be modified.
If we want to use the ResultSet to update data and traverse it in both directions, we need to create the Statement object with additional parameters:
stmt = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE );
To navigate this type of ResultSet, we can use one of the methods:
- first(), last(), beforeFirst(), beforeLast() – to move to the first or last line of a ResultSet or to the line before these
- next(), previous() – to navigate forward and backward in the ResultSet
- getRow() – to obtain the current row number
- moveToInsertRow(), moveToCurrentRow() – to move to a new empty row to insert and back to the current one if on a new row
- absolute(int row) – to move to the specified row
- relative(int nrRows) – to move the cursor the given number of rows
Updating the ResultSet can be done using methods with the format updateX() where X is the type of the cell data. These methods only update the ResultSet object and not the database tables.
To persist the ResultSet changes to the database, we must further use one of the methods:
- updateRow() – to persist the changes to the current row to the database
- insertRow(), deleteRow() – to add a new row or delete the current one from the database
- refreshRow() – to refresh the ResultSet with any changes in the database
- cancelRowUpdates() – to cancel changes made to the current row
Let’s take a look at an example of using some of these methods by updating data in the employees table:
Statement updatableStmt = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet updatableResultSet = updatableStmt.executeQuery(selectSql); updatableResultSet.moveToInsertRow(); updatableResultSet.updateString("name", "mark"); updatableResultSet.updateString("position", "analyst"); updatableResultSet.updateDouble("salary", 2000); updatableResultSet.insertRow();
The ResultSet object can be closed by using it in a try-with-resources block or by calling the close() method.
6. Parsing Metadata
The JDBC API allows looking up information about the database, called metadata.
6.1. DatabaseMetadata
The DatabaseMetadata interface can be used to obtain general information about the database such as the tables, stored procedures or SQL dialect.
Let’s have a quick look at how we can retrieve information on the database tables:
DatabaseMetaData dbmd = con.getMetaData(); ResultSet tablesResultSet = dbmd.getTables(null, null, "%", null); while (tablesResultSet.next()) { LOG.info(tablesResultSet.getString("TABLE_NAME")); }
6.2. ResultSetMetadata
This interface can be used to find information about a certain ResultSet, such as the number and name of its columns:
ResultSetMetaData rsmd = rs.getMetaData(); int nrColumns = rsmd.getColumnCount(); IntStream.range(1, nrColumns).forEach(i -> { try { LOG.info(rsmd.getColumnName(i)); } catch (SQLException e) { e.printStackTrace(); } });
7. Handling Transactions
By default, each SQL statement is committed right after it is completed. However, it’s also possible to control transactions programmatically.
This may be necessary in cases when we want to preserve data consistency, for example when we only want to commit a transaction if a previous one has completed successfully.
First, we need to set the autoCommit property of Connection to false, then use the commit() and rollback() methods to control the transaction.
Let’s add a second update statement for the salary column after the employee position column update and wrap them both in a transaction. This way, the salary will be updated only if the position was successfully updated:
String updatePositionSql = "UPDATE employees SET position=? WHERE emp_id=?"; PreparedStatement pstmt = con.prepareStatement(updatePositionSql); pstmt.setString(1, "lead developer"); pstmt.setInt(2, 1); String updateSalarySql = "UPDATE employees SET salary=? WHERE emp_id=?"; PreparedStatement pstmt2 = con.prepareStatement(updateSalarySql); pstmt.setDouble(1, 3000); pstmt.setInt(2, 1); boolean autoCommit = con.getAutoCommit(); try { con.setAutoCommit(false); pstmt.executeUpdate(); pstmt2.executeUpdate(); con.commit(); } catch (SQLException exc) { con.rollback(); } finally { con.setAutoCommit(autoCommit); }
8. Closing the Connection
When we’re no longer using, it’s necessary to close the connection to release database resources.
This can be done by using the close() API:
con.close();
9. Conclusion
In this tutorial, we had a look at the basics of working with the JDBC API.
As always, the full source code of the examples can be found over on GitHub.