You are on page 1of 16

1

JDBC includes four components:

1. The JDBC API

The JDBC™ API provides programmatic access to relational data from


the Java™ programming language.Using the JDBC API, applications can
execute SQL statements, retrieve results, and propagate changes back
to an underlying data source.

2. JDBC Driver Manager

The JDBC DriverManager class defines objects which can connect Java
applications to a JDBC driver. The Standard Extension packages javax.naming
and javax.sql let you use a DataSource object registered with a Java Naming
and Directory Interface™ (JNDI) naming service to establish a connection with
a data source. You can use either connecting mechanism, but using a
DataSource object is recommended whenever possible.

3. JDBC Test Suite

The JDBC driver test suite helps you to determine that JDBC drivers will
run your program

4. JDBC-ODBC Bridge

The Java Software bridge provides JDBC access via ODBC drivers.

JDBC™ application connects to a target data source using one of two mechanisms:

• DriverManager: This fully implemented class requires an application to load a specific


driver, using a hardcoded URL. As part of its initialization, the DriverManager class
attempts to load the driver classes referenced in the jdbc.drivers system property.

DataSource: This interface is preferred over DriverManager because it allows


details about the underlying data source to be transparent to your application. A
DataSource object's properties are set so that it represents a particular data source.

Establishing a connection involves two steps: Loading the driver, and making the
connection.

Loading the Driver

Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
Calling the Class.forName automatically creates an instance of a driver and registers
it with the DriverManager, so you don't need to create an instance of the class. If you
were to create your own instance, you would be creating an unnecessary duplicate,
but it would do no harm.
2

Making the Connection

1. Using the DriverManager Class

Calling the Class.forName automatically creates an instance of a driver and registers


it with the DriverManager, so you don't need to create an instance of the class.

Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
String url = "jdbc:derby:Fred";
Connection con = DriverManager.getConnection(url, "login name", "password")

url
the JDBC URL; parts one and two are supplied by your driver, and the third part specifies
your data source
myLogin
your login name or user name
myPassword
your password for the DBMS
myDriver.ClassName
the class name supplied with your driver
Class.forName("myDriver.ClassName");

2. Using a DataSource Object for a connection

Using a DataSource object increases application portability by making it possible for an


application to use a logical name for a data source instead of having to supply
information specific to a particular driver

InitialContext ic = new InitialContext()

DataSource ds = ic.lookup("java:comp/env/jdbc/myDB");
Connection con = ds.getConnection();
DataSource ds = (DataSource) org.apache.derby.jdbc.ClientDataSource()
ds.setPort(1527);
ds.setHost("localhost");
ds.setUser("APP")
ds.setPassword("APP");

Connection con = ds.getConnection();

The ResultSet interface provides methods for retrieving and manipulating the
results of executed queries, and ResultSet objects can have different functionality
and characteristics.

The sensitivity of the ResultSet object is determined by one of three different ResultSet types:

1. TYPE_FORWARD_ONLY — The result set is not scrollable; its cursor moves forward only,
from before the first row to after the last row. The rows contained in the result set depend
3

on how the underlying database materializes the results. That is, it contains the rows that
satisfy the query at either the time the query is executed or as the rows are retrieved.
2. TYPE_SCROLL_INSENSITIVE — The result set is scrollable; its cursor can move both
forward and backward relative to the current position, and it can move to an absolute
position.
3. TYPE_SCROLL_SENSITIVE — The result set is scrollable; its cursor can move both
forward and backward relative to the current position, and it can move to an absolute
position.

Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,


ResultSet.CONCUR_READ_ONLY);
ResultSet srs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES");

When a ResultSet object is first created, the cursor is positioned before the first row. To move
the cursor, you can use the following methods:

• next() - moves the cursor forward one row. Returns true if the cursor is now positioned
on a row and false if the cursor is positioned after the last row.
• previous() - moves the cursor backwards one row. Returns true if the cursor is now
positioned on a row and false if the cursor is positioned before the first row.
• first() - moves the cursor to the first row in the ResultSet object. Returns true if the
cursor is now positioned on the first row and false if the ResultSet object does not
contain any rows.
• last() - moves the cursor to the last row in the ResultSet object. Returns true if the
cursor is now positioned on the last row and false if the ResultSet object does not
contain any rows.
• beforeFirst() - positions the cursor at the start of the ResultSet object, before the first
row. If the ResultSet object does not contain any rows, this method has no effect.
• afterLast() - positions the cursor at the end of the ResultSet object, after the last row.
If the ResultSet object does not contain any rows, this method has no effect.
• relative(int rows) - moves the cursor relative to its current position.
• absolute(int row) - positions the cursor on the row-th row of the ResultSet object.

The ResultSet interface declares getter methods (getBoolean, getLong, and so on)
for retrieving column values from the current row. Your application can retrieve
values using either the index number of the column or the name of the column. The
column index is usually more efficient. Columns are numbered from 1. For
maximum portability, result set columns within each row should be read in left-to-
right order, and each column should be read only once.
4

Column names used as input to getter methods are case insensitive. When a getter
method is called with a column name and several columns have the same name,
the value of the first matching column will be returned.

Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,


ResultSet.CONCUR_READ_ONLY);
ResultSet srs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES");
srs.afterLast();
while (srs.previous()) {
String name = srs.getString("COF_NAME");
float price = srs.getFloat("PRICE");
System.out.println(name + " " + price);
}

following line of code moves the cursor to the fourth row of srs:

srs.absolute(4);

If srs has 500 rows, the following line of code moves the cursor to row 497:

srs.absolute(-4);
srs.absolute(4); // cursor is on the fourth row
. . .
srs.relative(-3); // cursor is on the first row
. . .
srs.relative(2); // cursor is on the third row

The method getRow lets you check the number of the row where the cursor is
positioned.

The position is stated in their names: isFirst, isLast, isBeforeFirst, isAfterLast.


These methods all return a boolean and can therefore be used in a conditional
statement

If the method isAfterLast returns false, the cursor is not after the last row, so the
method afterLast is invoked.

if (srs.isAfterLast() == false) {
srs.afterLast();
}
while (srs.previous()) {
String name = srs.getString("COF_NAME");
float price = srs.getFloat("PRICE");
System.out.println(name + " " + price);
}

Updating Tables
5

Updating a row in a ResultSet object is a two-phase process. First, the new value for
each column being updated is set, and then the change is applied to the row. The
row in the underlying data source is not updated until the second phase is
completed.

First, you need to create a ResultSet object that is updatable. To do this, supply the
ResultSet constant CONCUR_UPDATABLE to the createStatement method, Statement
object it creates produces an updatable ResultSet object each time it executes a
query

Connection con = DriverManager.getConnection("jdbc:mySubprotocol:mySubName");


Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
ResultSet uprs = stmt.executeQuery("SELECT COF_NAME, PRICE FROM COFFEES");

The ResultSet object uprs looks something like this:

COF_NAME PRICE
------------------ -----
Colombian 7.99
French_Roast 8.99
Espresso 9.99
Colombian_Decaf 8.99
French_Roast_Decaf 9.99
The method updateRow applies all column changes to the current row. The changes
are not made to the row until updateRow has been called. You can use the
cancelUpdates method to back out changes made to the row before the updateRow
method is called.

Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY,


ResultSet.CONCUR_UPDATABLE);
ResultSet srs = stmt.executeQuery("select COF_Name from COFFEES " +
"where price = 7.99");
srs.next();
srs.updateString("COF_NAME", "Foldgers");
srs.updateRow();

An update is the modification of a column value in the current row. Let's suppose that you want
to raise the price of French Roast Decaf coffee to 10.99:

uprs.last();
uprs.updateFloat("PRICE", 10.99);
uprs.updateRow();

Update operations affect column values in the row where the cursor is positioned, so in the first
line the ResultSet uprs calls the method last to move its cursor to the last row (the row where
the column COF_NAME has the value FRENCH_ROAST_DECAF). Once the cursor is on the last row,
6

all of the update methods you call will operate on that row until you move the cursor to another
row. The second line changes the value in the PRICE column to 10.99 by calling the method
updateFloat. This method is used because the column value we want to update is a float in the
Java programming language.

The ResultSet. updateXXX methods take two parameters: the column to update and the new
value to put in that column. As with the ResultSet.getXXX methods, the parameter designating
the column may be either the column name or the column number. There is a different
updateXXX method for updating each datatype ( updateString, updateBigDecimal,
updateInt, and so on) just as there are different getXXX methods for retrieving different
datatypes.

If you had moved the cursor to a different row before calling the method updateRow, the update
would have been lost. If, on the other hand, you realized that the price should really have been
10.79 instead of 10.99, you could have cancelled the update to 10.99 by calling the method
cancelRowUpdates. You have to invoke cancelRowUpdates before invoking the method
updateRow; once updateRow is called, calling the method cancelRowUpdates does nothing.
Note that cancelRowUpdates cancels all of the updates in a row, so if there are many
invocations of the updateXXX methods on the same row, you cannot cancel just one of them. The
following code fragment first cancels updating the price to 10.99 and then updates it to 10.79:

uprs.last();
uprs.updateFloat("PRICE", 10.99);
uprs.cancelRowUpdates();
uprs.updateFloat("PRICE", 10.79);
uprs.updateRow();

Using Prepared Statements


the PreparedStatement object contains not just an SQL statement, but an SQL
statement that has been precompiled. This means that when the PreparedStatement
is executed, the DBMS can just run the PreparedStatement SQL statement without
having to compile it first.

PreparedStatement updateSales = con.prepareStatement(


"UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?");
updateSales.setInt(1, 75);
updateSales.setString(2, "Colombian");

Code Fragment 1:

String updateString = "UPDATE COFFEES SET SALES = 75 " +


"WHERE COF_NAME LIKE 'Colombian'";
stmt.executeUpdate(updateString);

Code Fragment 2:

PreparedStatement updateSales = con.prepareStatement(


7

"UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ? ");


updateSales.setInt(1, 75);
updateSales.setString(2, "Colombian");
updateSales.executeUpdate():

Return Values for the executeUpdate Method


Whereas executeQuery returns a ResultSet object containing the results of the query
sent to the DBMS, the return value for executeUpdate is an int that indicates how
many rows of a table were updated.

Note that when the return value for executeUpdate is 0, it can mean one of two things:

• the statement executed was an update statement that affected zero rows

• the statement executed was a DDL statement.

Using Transactions
A transaction is a set of one or more statements that are executed together as a
unit, so either all of the statements are executed, or none of the statements is
executed.

When a connection is created, it is in auto-commit mode. This means that each individual SQL
statement is treated as a transaction and is automatically committed right after it is executed.
The way to allow two or more statements to be grouped into a transaction is to disable auto-
commit mode. This is demonstrated in the following line of code, where con is an active
connection:

con.setAutoCommit(false);

con.setAutoCommit(false);
PreparedStatement updateSales = con.prepareStatement(
"UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?");
updateSales.setInt(1, 50);
updateSales.setString(2, "Colombian");
updateSales.executeUpdate();
PreparedStatement updateTotal = con.prepareStatement(
"UPDATE COFFEES SET TOTAL = TOTAL + ? WHERE COF_NAME LIKE ?");
updateTotal.setInt(1, 50);
updateTotal.setString(2, "Colombian");
8

updateTotal.executeUpdate();
con.commit();
con.setAutoCommit(true);

To avoid conflicts during a transaction, a DBMS uses locks, mechanisms for blocking
access by others to the data that is being accessed by the transaction. (Note that in
auto-commit mode, where each statement is a transaction, locks are held for only
one statement.) Once a lock is set, it remains in force until the transaction is
committed or rolled back. For example, a DBMS could lock a row of a table until
updates to it have been committed. The effect of this lock would be to prevent a
user from getting a dirty read, that is, reading a value before it is made permanent.
(Accessing an updated value that has not been committed is considered a dirty read
because it is possible for that value to be rolled back to its previous value. If you
read a value that is later rolled back, you will have read an invalid value.)

if the transaction isolation level is set to TRANSACTION_READ_COMMITTED, the DBMS


does not allow dirty reads to occur. The interface Connection includes five values
which represent the transaction isolation levels you can use in JDBC.

Normally, you do not need to do anything about the transaction isolation level; you
can just use the default one for your DBMS. JDBC allows you to find out what
transaction isolation level your DBMS is set to (using the Connection method
getTransactionIsolation) and also allows you to set it to another level (using the
Connection method setTransactionIsolation). Keep in mind, however, that even
though JDBC allows you to set a transaction isolation level, doing so has no effect
unless the driver and DBMS you are using support it.

Setting and Rolling Back to a Savepoint

The JDBC 3.0 API adds the method Connection.setSavepoint, which sets a savepoint within
the current transaction. The Connection.rollback method has been overloaded to take a
savepoint argument.

The example below inserts a row into a table, sets the savepoint svpt1, and then inserts a second
row. When the transaction is later rolled back to svpt1, the second insertion is undone, but the
first insertion remains intact. In other words, when the transaction is committed, only the row
containing ?FIRST? will be added to TAB1:

Statement stmt = conn.createStatement();


int rows = stmt.executeUpdate("INSERT INTO TAB1 (COL1) VALUES " +
9

"(?FIRST?)");
// set savepoint
Savepoint svpt1 = conn.setSavepoint("SAVEPOINT_1");
rows = stmt.executeUpdate("INSERT INTO TAB1 (COL1) " +
"VALUES (?SECOND?)");
...
conn.rollback(svpt1);
...
conn.commit();

Releasing a Savepoint

The method Connection.releaseSavepoint takes a Savepoint object as a parameter and


removes it from the current transaction.

Once a savepoint has been released, attempting to reference it in a rollback operation causes an
SQLException to be thrown. Any savepoints that have been created in a transaction are
automatically released and become invalid when the transaction is committed, or when the entire
transaction is rolled back. Rolling a transaction back to a savepoint automatically releases and
makes invalid any other savepoints that were created after the savepoint in question.

When to Call the Method rollback

As mentioned earlier, calling the method rollback aborts a transaction and returns any values
that were modified to their previous values. If you are trying to execute one or more statements
in a transaction and get an SQLException, you should call the method rollback to abort the
transaction and start the transaction all over again. That is the only way to be sure of what has
been committed and what has not been committed. Catching an SQLException tells you that
something is wrong, but it does not tell you what was or was not committed. Since you cannot
count on the fact that nothing was committed, calling the method rollback is the only way to be
sure.

Stored procedures are used to encapsulate a set of operations or queries to execute


on a database server.

A callableStatement object contains a call to a stored procedure; it does not contain the stored
procedure itself. The first line of code below creates a call to the stored procedure
SHOW_SUPPLIERS using the connection con. The part that is enclosed in curly braces is the escape
syntax for stored procedures. When the driver encounters "{call SHOW_SUPPLIERS}", it will
translate this escape syntax into the native SQL used by the database to call the stored procedure
named SHOW_SUPPLIERS.
10

CallableStatement cs = con.prepareCall("{call SHOW_SUPPLIERS}");


ResultSet rs = cs.executeQuery();

try {
// Code that could generate an exception goes here.
// If an exception is generated, the catch block below
// will print out information about it.
} catch(SQLException ex) {
System.out.println("\n--- SQLException caught ---\n");
while (ex != null) {
System.out.println("Message: "
+ ex.getMessage ());
System.out.println("SQLState: "
+ ex.getSQLState ());
System.out.println("ErrorCode: "
+ ex.getErrorCode ());
ex = ex.getNextException();
System.out.println("");
}
}

Statement stmt = con.createStatement();


ResultSet rs = stmt.executeQuery("select COF_NAME from COFFEES");
while (rs.next()) {
String coffeeName = rs.getString("COF_NAME");
System.out.println("Coffees available at the Coffee Break: ");
System.out.println(" " + coffeeName);
SQLWarning warning = stmt.getWarnings();
if (warning != null) {
System.out.println("\n---Warning---\n");
while (warning != null) {
System.out.println("Message: "
+ warning.getMessage());
System.out.println("SQLState: "
+ warning.getSQLState());
System.out.print("Vendor error code: ");
System.out.println(warning.getErrorCode());
System.out.println("");
warning = warning.getNextWarning();
}
11

http://java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/GettingStartedTOC.fm.html

The standard syntax for JDBC URLs is shown here. It has three parts, which are separated by
colons.

jdbc:<subprotocol>:<subname>

The three parts of a JDBC URL are broken down as follows:

1. jdbc-the protocol. The protocol in a JDBC URL is always jdbc.


2. <subprotocol>-the name of the driver or the name of a database connectivity
mechanism, which may be supported by one or more drivers. A prominent example of a
subprotocol name is odbc, which has been reserved for URLs that specify ODBC-style
data source names. For example, to access a database through a JDBC-ODBC bridge, one
might use a URL such as the following:
3. jdbc:odbc:fred

In this example, the subprotocol is odbc, and the subname fred is a local ODBC data
source.

If one wants to use a network name service (so that the database name in the JDBC URL
does not have to be its actual name), the naming service can be the subprotocol. So, for
example, one might have a URL like:

jdbc:dcenaming:accounts-payable

In this example, the URL specifies that the local DCE naming service should resolve the
database name accounts-payable into a more specific name that can be used to connect
to the real database.

4. <subname>-a way to identify the data source. The subname can vary, depending on the
subprotocol, and it can have any internal syntax the driver writer chooses, including a
subsubname. The point of a subname is to give enough information to locate the data
source. In the previous example, fred is enough because ODBC provides the remainder
of the information. A data source on a remote server requires more information, however.
If the data source is to be accessed over the Internet, for example, the network address
should be included in the JDBC URL as part of the subname and should adhere to the
following standard URL naming convention:
5. //hostname:port/subsubname

Supposing that dbnet is a protocol for connecting to a host on the Internet, a JDBC URL
might look like this:
12

jdbc:dbnet://wombat:356/fred

Keeping Track of Available Drivers

The DriverManager class maintains a list of Driver classes that have registered themselves by
calling the method DriverManager.registerDriver. All Driver classes should be written with
a static section (a static initializer) that creates an instance of the class and then registers it with
the DriverManager class when it is loaded. Thus, a user would not normally call
DriverManager.registerDriver directly; it should be called automatically by a Driver class
when it is loaded. A Driver class is loaded, and therefore automatically registered with the
DriverManager, in one of two ways:

1. by calling the method Class.forName. This explicitly loads the driver class. Since it
does not depend on any external setup, this way of loading a driver is the recommended
one for using the DriverManager framework. The following code loads the class
acme.db.Driver:
2. Class.forName("acme.db.Driver");

If acme.db.Driver has been written so that loading it causes an instance to be created


and also calls DriverManager.registerDriver with that instance as the parameter (as it
should do), then it is in the DriverManager's list of drivers and available for creating a
connection.

3. by adding the Driver class to the java.lang.System property jdbc.drivers. This is a


list of driver classnames, separated by colons, that the DriverManager class loads. When
the DriverManager class is initialized, it looks for the system property "jdbc.drivers,"
and if the user has entered one or more drivers, the DriverManager class attempts to load
them. The following code illustrates how a programmer might enter three driver classes
in ~/.hotjava/properties (HotJava loads these into the system properties list on startup):

jdbc.drivers=foo.bah.Driver:wombat.sql.Driver:bad.test.ourDriver
The first call to a DriverManager method will automatically cause these driver classes to be
loaded.
Note that this second way of loading drivers requires a preset environment that is
persistent. If there is any doubt about that being the case, it is safer to call the
method Class.forName to explicitly load each driver. This is also the right method to
use to bring in a particular driver since once the DriverManager class has been
initialized, it will never recheck the jdbc.drivers property list.

Establishing a Connection
13

Once the Driver classes have been loaded and registered with the DriverManager class, they
are available for establishing a connection with a database. When a request for a connection is
made with a call to the DriverManager.getConnection method, the DriverManager tests each
driver in turn to see if it can establish a connection.

It may sometimes be the case that more than one JDBC driver is capable of connecting to a given
URL. For example, when connecting to a given remote database, it might be possible to use a
JDBC-ODBC bridge driver, a JDBC-to-generic-network-protocol driver, or a driver supplied by
the database vendor. In such cases, the order in which the drivers are tested is significant because
the DriverManager will use the first driver it finds that can successfully connect to the given
URL.

First the DriverManager tries to use each driver in the order it was registered. (The drivers listed
in jdbc.drivers are always registered first.) It will skip any drivers that are untrusted code
unless they have been loaded from the same source as the code that is trying to open the
connection.

It tests the drivers by calling the method Driver.connect on each one in turn, passing them the
URL that the user originally passed to the method DriverManager.getConnection. The first
driver that recognizes the URL makes the connection.

The following code is an example of all that is normally needed to set up a connection with a
driver such as a JDBC-ODBC bridge driver.

Class.forName("jdbc.odbc.JdbcOdbcDriver"); //loads the driver


String url = "jdbc:odbc:fred";
Connection con = DriverManager.getConnection(
url, "userID", "passwd");

The variable con represents a connection to the data source "fred" that can be used to create and
execute SQL statements.

DriverManager Methods Are Static

All DriverManager methods are declared static, which means that they operate on the class as
a whole and not on particular instances. In fact, the constructor for DriverManager is declared
private to prevent users from instantiating it. Logically, there is one instance of the
DriverManager class.
14

http://www.javacamp.org/moreclasses/jdbc/jdbc.html

There are seven standard steps in querying databases:

1. Load the JDBC driver.


2. Define the connection URL.
3. Establish the connection.
4. Create a statement object.
5. Execute a query or update.
6. Process the results.
7. Close the connection.

Step 1: Load the JDBC driver

To load the driver, you need to load the appropriate class, make a driver instance and register it
with the JDBC driver manager. Use Class.forName(String) method. This method takes a string
representing a fully qualified class name and loads the corresponding class.

try {
Class.forName("connect.microsoft.MicrosoftDriver");
//Class.forName("oracle.jdbc.driver.OracleDriver"); for Oracle driver
//Class.forName("com.sybase.jdbc.SybDriver"); for sybase driver
} catch(ClassNotFoundException e) {
System.err.println("Error loading driver: " + e);
}

Step 2: Define the Connection URL

Once you have loaded the JDBC driver, you need to specify the location of the database server.
Generally, the url format is: jdbc:vendordb:userinfo plus server host, port number and database
name.

//for one of Oracle drivers


String host = "dbhost.yourcompany.com";
String dbName = "someName";
int port = 1234;
String oracleURL = "jdbc:oracle:thin:@" + host +
":" + port + ":" + dbName;

//for sybase driver


String host = "dbhost.yourcompany.com";
String dbName = "someName";
15

int port = 1234;


String sybaseURL = "jdbc:sybase:Tds:" + host +
":" + port + ":" + "?SERVICENAME=" + dbName;

Step 3: Establish the connection

To make the actual network connection, pass the URL, the database username, and the password
to the getConnection method of the DriverManager class, as illustrated in the following example.

String username = "jay_debesee";


String password = "secret";
Connection connection =
DriverManager.getConnection(oracleURL, username, password);

Step 4: Create a Statement

A Statement object is used to send queries and commands to the database and is created from the
Connection as follows:

Statement statement = connection.createStatement();

Step 5: Execute a Query or update

Once you have a Statement object, you can use it to send SQL queries by using the executeQuery
method, which returns an object of type ResultSet. Here is an example:

String query = "SELECT col1, col2, col3 FROM sometable";


ResultSet resultSet = statement.executeQuery(query);

Step 6: Process the Results

The simplest way to handle the results is to process them one row at a time, using the ResultSet’s
next method to move through the table a row at a time. Within a row, ResultSet provides various
getXxx methods that take a column index or column name as an argument and return the result
as a variety of different Java types. For instance, use getInt if the value should be an integer,
getString for a String, and so on for most other data types. If you just want to display the results,
you can use getString regardless of the actual column type. However, if you use the version that
takes a column index, note that columns are indexed starting at 1 (following the SQL
16

convention), not at 0 as with arrays, vectors, and most other data structures in the Java
programming language.

while(resultSet.next()) {
System.out.println(results.getString(1) + " " +
results.getString(2) + " " +
results.getString(3));
}

Step 7: Close the Connection

To close the connection explicitly, you should do:

connection.close();

You should postpone this step if you expect to perform additional database operations, since the
overhead of opening a connection is usually large. In fact, reusing existing connections is such an
important optimization.

You might also like