Database Access
In this chapter, you will learn about the objects needed to access a DBMS (Database Management System). In addition, we will cover the architectural topic of data encapsulation and use SQL as the data access language. We will then demonstrate how to configure the RAD and PC environment to connect to a DBMS and how to access the database from Java applications.
In this chapter, you will learn about:
The following objects:
Class
DriverManager
Statement
Connection
ResultSet
After this chapter, you should be able to:
Databases hold the information most organizations need to perform their daily activities. For instance, a grocery store keeps all their prices in a database. When you go through checkout, the price for each item in the cart is retrieved and used to calculate the total amount owed. Databases are so essential that if the database is unavailable, most applications cannot function. In Java-speak this is because DBMSs provide “data persistence.” Java objects do not provide data persistence. In other words, when an object is deleted any information the object contained is also deleted. With a DBMS, data is safely stored and accessible whether an object exists or not.
Applications don’t have to use databases. However, without a database, applications are very difficult and time-consuming to use. For instance, would a payroll application be easy to use if all the employee’s information had to be entered each time the application was run? If instead all the employee information is entered into a database once, the application can access that information at any time.
Establishing a connection to a database is a little cumbersome but, if coded wisely, only has to be specified once. In fact, a primary goal is to hide (from the programmers) the specifics of how to access a database. Organizations try to hide the specifics such that Java programmers have no idea what DBMS is being accessed or even where the database is located. This “design” goal goes by many names such as “data encapsulation,” “transparency,” “information hiding,” and/or “implementation ignorance.”
First, we will explain and demonstrate the basics for accessing a database and then show how to efficiently define and use the necessary classes/objects.
Several new classes will help establish a database connection and then access the database. However, there is one piece of the puzzle, the driver, which does not “come with” Java. This is because the individual DBMS vendors must supply the driver software to access their DBMS. In other words, the Java developers specify how a database driver must work, but it is up to the DBMS provider (IBM, Oracle, Microsoft, etc.) to create and supply the software for Java to work with their DBMS (DB2, Oracle, Access, etc.). Most DBMS providers are very eager to supply drivers because if there is no driver, Java applications cannot use the DBMS. Of course, this means they cannot sell you their DBMS. You can tell that the DBMS providers are very eager to supply the driver software because they provide it for free! When downloaded, a driver is usually supplied as a ZIP or JAR file. (Microsoft, however, includes the driver for its Access DBMS within the Windows operating system.)
To use the driver, you must identify the driver’s location (on the PC) to RAD. One way to do this is to add the driver location to the “Java Build Path.” RAD uses the Java Build Path to find classes that are not in the project. Therefore, we need to add an external JAR to RAD’s Java Build Path. For this discussion, we have assumed that the driver (in a JAR) is on the PC’s C: drive. (Microsoft Access, however, is different. The MS Access driver must be identified when defining an Access DB as an ODBC data source in Windows.)
Once the driver is identified, the Java class must load the driver. You can think of loading the driver as creating an instance of the driver class. However, as with the formatting classes (covered in an earlier chapter), the programmer does not explicitly create the driver object. Instead, the Class class’s forName method is used. (Yes, there is a class called Class!) The forName method returns an object of the class type specified. For instance, the following statement:
Class.forName("com.ibm.as400.access.AS400JDBCDriver");
returns a driver object that can be used to access a DB2 database on a Power System (an IBM midrange computer).
Next, a Connection object must be created. Once again, you do not directly create this object. Instead, you use the DriverManager class’s getConnection method. The getConnection method requires the following information to create the connection:
A Statement object then needs to be created. Statement objects have execute methods that allow the programmer to run SQL commands that insert, retrieve, delete, or update data in the database. Again, the programmer does not explicitly create the needed object. Instead, the Connection object’s createStatement method is used.
To review, the application environment must first be set up correctly. This means:
Then a Java class must do the following to access a database:
First, we will perform the generic tasks that are required regardless of the DBMS being used. Then there will be three tutorials showing how to connect to three different types of DBMSs. The way the tutorials are written, you can only do one of these. (If you do not have access to an Oracle database or DB2 on a Power System, do the “Set Up for Accessing a Microsoft Access Database” tutorial. Even though Access is rarely used for commercial applications, it is widely available and this tutorial will give you a taste of database programming.) We assume that each of these DBMSs has a database called tntdb and a table called Employee with fields that correspond to the Employee class’s properties.
Let’s do it!
Tutorial: Setting up the Project
The first task is to set up the project.
A new class called DBAccess is created in c10 and the source code will be displayed.
public DBAccess() { }
We will now add code to the main method so that we can test the class.
DBAccess dba = new DBAccess();
Tutorial: Accessing DB2 on an IBM Power System
You must first download the driver. As of this writing, IBM offers the driver free as part of a product called JTOpen To download the driver, go to http://sourceforge.net/projects/jt400/ (see Figure 10-1) and click the link for downloading the latest version of JTOpen as a single zip file. (Be aware that the Web is constantly changing and you may have to search for JTOpen.) For this tutorial, we assume that the file was downloaded to the F: drive and named jtopen_7_3.zip.
This assumes you have an unzip program already installed. If not, you will have to download one. A very popular version is WinZip. To unzip, simply double-click the zip file icon, follow the prompts, and at the “unzip to folder” window specify F:jtopen.
The JAR Selection window will be displayed.
The file jt400 will appear in the Libraries pane of the “Properties for Tutorials” window.
In the Package Explorer pane, expand the Tutorials project then Referenced Libraries. If jt400.jar is listed, you have successfully added the file to the Java Build Path.
We now need four strings in the DBAccess class to hold unique information about the DB2 database.
private String driver = new
String("com.ibm.as400.access.AS400JDBCDriver");
private String url = new String("jdbc:as400: 111.22.333.444 ");
private String userid = new String(" bjanson ");
private String pw = new String(" jeter ");
Notice that when specifying the URL, the protocol (jdbc:as400:) is specified before the IP address. As mentioned earlier, each DBMS will have a different protocol name and syntax for specifying the URL
Tutorial: Accessing an Oracle Database
You must first download the driver. As of this writing, Oracle offers the driver free and can be downloaded http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html (see Figure 10-3). Follow the links for downloading the driver that corresponds to the Oracle and JDK versions you are using. The arrow in Figure 10-3 points at the driver for Oracle 11g Release 2. Double-click the correct driver for your Oracle system.
A license agreement page will be displayed. Accept the terms. A page of JDK options will be displayed (see Figure 10-4). Oracle 11g supports many different JDK versions. RAD 8 can run at JDKs ranging up to 6.0, so choose the appropriate JDK for your installation of RAD. (Note: drivers for earlier JDK versions will work with later JDK versions. The arrow in Figure 10-4 points to a JDK 6.0 option).
Oracle, as with most vendors, will then require you to register with them and provide contact information. Select the option to save a zip file to a drive on your PC. For this tutorial, we will assume that the file is named ojdbc6.zip and that you saved it to the F: drive.
The “Properties for Tutorials” window will be displayed.
The file ojdbc6.zip will appear in the Libraries pane of the “Properties for Tutorials” window.
In the Package Explorer pane, expand Tutorials then Referenced Libraries. If ojdbc6.zip is listed, you have successfully added the file to the Java Build Path.
We will now define four strings to hold unique Oracle information.
private String driver = new
String("oracle.jdbc.driver.OracleDriver");
private String url = new
String("jdbc:oracle:thin:@ 111.22.333.444 :1521:SID");
private String userid = new String(" bjanson ");
private String pw = new String(" jeter ");
Notice that in the URL, the protocol (jdbc:oracle:thin) is specified before the IP address. In addition, for Oracle, you must specify a port and SID. As mentioned earlier, each DBMS will have a different protocol name and syntax for specifying the location of the DBMS.
Tutorial: Accessing a Microsoft Access Database
Microsoft supports a protocol called ODBC (Open Database Connectivity). You must identify the Microsoft Access Database as an ODBC data source within Windows. This tutorial assumes that a database called TNTDB was created in Access and has been imported into the project Tutorial
The ODBC Data Source Administrator window is displayed. On this window, you will select the correct ODBC driver for the MS Access database.
The ODBC Microsoft Access Setup window is redisplayed with the database location and name specified.
The ODBC Data Source Administrator window will now show the new Data Source Name (TNT Database).
We will now define four strings to hold the Microsoft Access DBMS information.
private String driver = new
String("sun.jdbc.odbc.JdbcOdbcDriver");
JdbcOdbcDriver is the driver/class we will be using to connect to the database. Fortunately, this class comes with RAD’s JRE and is in every project. (You can prove this by drilling down into the JRE System Library, in the Package Explorer view. Then expand rt.jar and the sun.jdbc.odbc package to show the JdbcOdbcDriver class file.)
private String url = new String("jdbc:odbc:TNT Database");
private String userid = new String("anonymous");
private String pw = new String("guest");
When specifying the URL (location) of the database, the protocol (jdbc:odbc:) is specified before the ODBC Data Source Name (TNT Database). Notice that the ODBC data source name is specified not TNTDB (the PC file name). Each DBMS will have a different protocol name and syntax for specifying the database location.
Tutorial: Set-Up for Accessing All Databases
The next stage is setting up access for our databases:
import java.sql.*;
This will give us access to various classes (like Connection) that we need to access the database.
Connection con = null ;
ResultSet rs = null ;
Statement stmt = null ;
Next, we will create and assign objects to these variables. We mentioned earlier how the Connection and Statement objects are used; however, this is the first time a result set has been mentioned.
When information is retrieved from a database using SQL, the information is returned in the form of an object called a result set. You can think of a result set as a table that can have multiple columns (fields) and rows (records) filled with data. To retrieve the field and record information, a variety of result set getter methods are used. Each result set getter is associated with a particular data type (i.e., getString, getDouble, etc.). We’ll show how to use these getters a little later.
try {
Class.forName(driver);
System.out.println("Driver class found!");
} catch (ClassNotFoundException e) {
System.out.println("Driver class not found: " + e);
}
We added try/catch statements because a ClassNotFoundException can be thrown by the forName method. In addition, we added println statements to help with testing and debugging. Notice that the catch block not only displays an appropriate message but also displays the exception object. Displaying an exception object provides extra information regarding the problem and is a good debugging practice.
try {
con = DriverManager.getConnection(url, userid, pw);
System.out.println("Connection created!");
con.close();
} catch (SQLException e) {
System.out.println("No connection: " + e);
}
The constructor now performs the first two steps mentioned earlier: registering the driver and getting a Connection object. Notice that we also closed the connection. You can think of a Connection object as a link to a database just as a telephone call is a link to person. When you are done with a connection, you should close it - just as when a phone conversation is finished, you should hang up the phone so that the line is not tied up.
The last thing we need is a Statement object.
stmt = con.createStatement();
System.out.println("Statement created!");
If the driver name and location were specified correctly, the following will be displayed in the Console view:
Driver class found!
Connection created!
Statement created!
Tutorial: Breaking the Code, Drivers, and Connections
Accessing databases can be very frustrating, especially when things go wrong because you really do not know what is going on. For instance, you use classes (like Class and DriverManager) with no idea of how they work. Often new programmers feel that if they knew how these classes worked, they could solve the problems that do occur. However, understanding how they work will not help and, trust me, you do not want to know how they work! How a connection to the database is made is complicated and understanding the process will not help solve the problems that are likely to arise. Usually things go wrong because of simple programming mistakes and typos, not because of the special database classes or the DBMS you are trying to access. We will replicate the most common mistakes and highlight some good testing techniques so you will be able to recognize and solve these common problems quickly.
The following message will appear in the Console:
Driver class not found: java.lang.ClassNotFoundException:
Xxx.xxxxx.xxxxDriver
where Xxx.xxxxx.xxxxDriver is the incorrectly capitalized driver name.
“Driver class not found” is a straightforward message, especially in this case, because we incorrectly specified the driver class name. The lesson is: be careful of capitalization, spelling, and the use of special characters (periods, forward slashes, colons, etc.) because when problems occur, these are the usual culprits.
Notice that this time we get the error message:
No connection:java.sql.SQLException : No suitable driver
“No suitable driver” is not very descriptive, but it means that the JVM could not find the correct driver. When the connection is attempted, the JVM searches the registered drivers for the correct driver. In this case, we specified the driver name correctly but did not register it, so the JVM cannot find the driver and an exception is thrown.
The following messages are displayed (with Xxxxxx being unique text for each DBMS):
Driver class found!
No connection: java.sql.SQLException : Xxxxxx
Notice that the driver was successfully registered but no connection was created because we did not specify the correct location. This error will also occur if the DBMS you are trying to access is not available. A DBMS may not be available for a variety of reasons. For example, the communication link between your computer and the DBMS could be down, the database could be off line for maintenance work, or the computer where the database resides is turned off. Usually this error is generated because the programmer has simply mistyped the IP address or URL.
The resulting message (or lack of message) depends on the driver used and whether the database you are trying to access is password protected. (For instance, the default for MS Access databases is no password-protection but the DB2 and Oracle examples returns a “Password is incorrect” message.) A similar error will be generated for mistakes with the user ID value.
Tutorial: Using a Statement Object to Insert Data
So far, we have created all the objects needed to insert, modify, retrieve, and delete data in a database. Now we just have to use those objects and methods. This section assumes knowledge of SQL. (If you are not familiar with SQL, please see Appendix B.)
Statement objects have two primary methods (executeQuery and executeUpdate) that allow the programmer to perform SQL statements in a Java class. Each method expects an SQL statement as a string. executeQuery is used for SQL select statements and will return a result set with the requested database information. executeUpdate is used for SQL insert, update, and delete statements and does not return data from the database. However, executeUpdate does provide the option of returning the number of rows affected by the SQL statement.
Each of the statements assumes that the database (schema in SQL-speak) tntdb has been created and a connection has been established. In addition, we assume that a table named employee was created in tntdb with the following SQL statement (please note that if Oracle is being used, payrate must be defined as NUMBER not DOUBLE):
CREATE TABLE tntdb.employee
(empnum CHAR(10), empname CHAR(25),
street CHAR(15), city CHAR(12),
state CHAR(2), zip CHAR(5),
payrate DOUBLE, exempts INT)
Enough talking, let’s get our hands dirty.
stmt.executeUpdate("INSERT INTO tntdb.employee " +
"VALUES('111', 'Mary Worker', '1 Main St.'," +
"'Enid', 'OK', '77777', 17.50, 3)");
Remember, the SQL statement is simply a string parameter for the executeUpdate method. RAD does not check for SQL syntax errors and, unfortunately, the statement syntax is complex. For instance, notice that both single and double quotes are used. Double quotes surround the entire SQL statement, but single quotes surround the values being inserted into character fields.
In addition, the SQL statement must be passed as a single string. If the string spans multiple lines, each line is a separate string (surrounded by double quotes) and must be concatenated to the others. Within the SQL statement, commas separate each value and there must be at least one space after the table name. Finally, the entire string is enclosed in parentheses. Expect many mistakes typing SQL statements, and try not to get too upset at yourself.
The three messages will appear in the Console but there will be no indication that the SQL statement successfully executed.
int returnValue;
returnValue = stmt.executeUpdate("INSERT INTO " +
"tntdb.employee(empnum, empname, payrate, exempts) " +
"VALUES('222', 'Joe Programmer', 17.50, 2)");
System. out .println(returnValue);
The following three messages and the number 1 will appear in the Console as follows:
Driver class found!
Connection created!
Statement created!
1
The one (1) indicates the number of rows affected by the executeUpdate and, in this case, proves that a row was inserted. However, a returned value of 1 does not instill a lot of confidence. So, we will prove that the data was correctly inserted by retrieving and displaying the data.
As mentioned, a SQL select statement returns data as a result set. Therefore, you must create a ResultSet variable and assign the ResultSet object (returned by the SQL statement) to that variable. Receiving the result set is only half the battle. You must then retrieve the data from the result set. Several ResultSet methods provide access to the data in a particular row. Which row is accessed is dictated by the result set’s cursor. Just as a word processor’s cursor indicates which line you are on in a document, a result set cursor indicates which row you are on in a table.
To retrieve data, the cursor must point to a row in the result set. The row indicated by the cursor is called the current row. Whenever an operation (like retrieving data) is executed, the operation is performed against the current row. The problem is that when a result set is returned from the SQL statement, the cursor is positioned before the first row (see Figure 10-8). The programmer must code a statement to move the cursor to the first row.
The cursor is moved sequentially between rows with the next method. Assuming that the ResultSet variable is named rs, executing the next method as follows:
rs.next();
positions the cursor to the first row as seen in Figure 10-9. Each time the next method is executed, the cursor is moved to the next row. In addition, when the cursor is successfully moved to the next row, a Boolean value of true is returned by the next method. If the cursor is at the last row and the next method is performed, a Boolean value of false will be returned. This Boolean return value is very useful. For example, it can be used to determine if the result set is empty.
Yes, it is possible for a select statement to return a result set with no rows. Therefore, to ensure that there is data to retrieve, you will want to check the returned value each time the next method is executed. Also, trying to retrieve data when the cursor is not pointing at a row, results in an SQL exception and the message "Cursor position not valid."
Once the cursor is pointing at a valid row, there is a series of ResultSet getter methods ::to retrieve the individual column values in the row. The programmer must use the correct getter method for the data type being retrieved and must specify which column is being retrieved. The three getters we will work with are getString, getInt, and getDouble. There are, of course, getters for the other data types. (A quick trip to the Oracle documentation website will show you all the available ResultSet methods.)
Again, assuming that rs is a valid ResultSet variable and the cursor is pointing at a row, the following would retrieve the empnum column’s text data and assign it to a String variable called empNum:
empNum = rs.getString(empnum);
In the above example, the column was identified by the column name. You can also reference a column by its number. For instance, assuming empNum is the first column, the following statement would also work:
empNum = rs.getString(1);
Unfortunately, the column number is dependent on the select statement. For instance, if the select statement had specified the fields as follows:
SELECT empname, empnum FROM tntdb.employee
empname would be column 1 and empnum would be column 2.
Generally, specifying column numbers and retrieving the result set columns from left to right provides the fastest retrieval.
Tutorial: Retrieving Data from a Database
Let’s get some data out of those databases:
rs = stmt.executeQuery("SELECT * FROM tntdb.employee");
System.out.println(rs.next());
System.out.println(rs.getString(1) + rs.getString(2));
System.out.println(rs.next());
System.out.println(rs.getString(1) + rs.getString(2));
System.out.println(rs.next());
The results will be the following:
Driver class found!
Connection created!
Statement created!
true
111 Mary Worker
true
222 Joe Programmer
false
Even though the select returned all the columns, we only retrieved data from the first two columns. This is not a terrible mistake. However, if only two columns are required, for efficiency’s sake, the select should specify only those columns.
Notice how inefficient the source code is. What if we want to retrieve 50 rows? Should we perform the gets 50 times each?! That does not seem right. We need to improve our data retrieval. In fact, we will use the returned Boolean values to build a loop to access the result set. We included the Boolean value println statements to demonstrate that the first two times the next method was executed there were rows to move the cursor to and the returned value was true. The third time next was executed there was no row, therefore, a false value was returned.
rs = stmt.executeQuery(
"SELECT empname, empnum, payrate, exempts" +
" FROM tntdb.employee");
This select will only retrieve four columns. Note that two of the columns contain non-string data.
while (rs.next()){
System.out.println(rs.getString(1) + rs.getString(2) +
rs.getDouble(3) + " " + rs.getInt(4)); }
This code defines a loop that moves the cursor to each row. When there are no more rows (i.e., the next method returns a false value), the loop will end. For each row, the appropriate getter retrieves each column’s data and the println statement displays the information.
The results will be as follows:
Driver class found!
Connection created!
Statement created!
Mary Worker 111 17.5 3
Joe Programmer 222 17.5 2
rs = stmt.executeQuery("SELECT * FROM tntdb.employee");
while (rs.next()){
System.out.println(rs.getString(1) + rs.getString(2) +
rs.getString(3) + rs.getString(4) + rs.getString(5) +
rs.getString(6) + rs.getDouble(7) + " " +
rs.getInt(8));
}
The results will be as follows:
Driver class found!
Connection created!
Statement created!
111 Mary Worker 1 Main St. Enid OK7777717.5 3
222 Joe Programmer nullnullnullnull17.5 2
Notice the null values for Joe Programmer. In the original insert statement, no address information was supplied, so the DBMS assigned null values to those fields. We will rectify this by using the update command to change the information in the existing row for Joe Programmer.
stmt.executeUpdate("UPDATE tntdb.employee " +
"SET street = '2 Maple Ave.', " +
"city = 'Enid', " +
"state = 'OK', " +
"zip = '77777', payrate = 19 " +
"WHERE empnum = '222'");
This update command not only supplies information for the address fields (that previously had null values) but also changes the pay rate value for employee number 222. Beware: if no WHERE clause had been specified, all the rows in the table would have been changed and all the rows would have had the same values for the address columns and payrate. The update command is powerful, but dangerous if used incorrectly!
The results will be as follows:
Driver class found!
Connection created!
Statement created!
111 Mary Worker 1 Main St. Enid OK 7777717.5 3
222 Joe Programmer 2 Maple Ave. Enid OK 7777719.0 2
Encapsulationis a design goal for Java classes. Encapsulation (also called class abstraction) tries to hide the internal workings of a class from users of the class (called clients). This means that clients cannot directly access the data or any “internal” objects used by the class to perform its function. Essentially, this means that internal variables and methods are defined as private and the encapsulated class provides easy-to-use public methods to manipulate the private variables and invoke the internal methods. These public methods define the class’s client interface. Encapsulation is good because it simplifies using the class and cuts down on client errors.
There is no better example of the benefits of encapsulation than with a class that accesses a database. Certainly accessing a database was not easy. Connection objects, driver managers, and statement objects are enough to drive any programmer nuts! Why not hide all those objects and the required coding inside of private class methods and have clients execute relatively simple public methods to retrieve and insert to the database?
In the following example, we will also put inheritance to good use and define the various objects needed to access the database only once in a superclass. All classes that provide DBMS access will be defined as subclasses thereby inheriting the needed variables, objects, and methods from the superclass. For instance, we already created a class called DBAccess with all the needed objects. Instead of redefining the database objects in Employee, Employee will be declared a subclass of DBAccess and inherit them. (We will have to modify DBAccess a little.) If any other classes need database access, they will also be defined as subclasses of DBAccess. This is going to save you a lot of typing and errors.
Finally, we are going to modify the employee application to provide the ability to insert, update, and display employee information stored in TNTDB. When a user selects the Employee Application option from AppOptions (see Figure 10-10), the new database options will be displayed in a frame called EmpOptions (see Figure 10-11).
When a user selects the display or update options on EmpOptions, a new frame, EmpNumFrame, will be displayed. EmpNum-Frame’s choice box will contain all the employee numbers from TNTDB, so that the user can click on which employee’s information (i.e., employee number) to update or display (see Figure 10-12). We will modify the Employee and EnterEmpInfo classes to support these new features.
Tutorial: Using a Database in an Application
Now it’s time to use the database:
AppOptions EmployeeFrame EnterEmpInfo
ExitButton TNT TNTButton
UsefulFrame
There will be several errors displayed because c10 doesn’t have an Employee class.
Driver class found!
Connection created!
Statement created!
The executable code should look like the following (with your information substituted in the driver, url, userid and pw strings):
package c10;
import java.sql.*;
public class DBAccess {
private String driver = new String("xxx.xxx.xDriver");
private String url = new String("jdbc:xxx:111.22.333.444");
private String userid = new String("xxx");
private String pw = new String("xxxx");
Connection con = null ;
ResultSet rs = null ;
Statement stmt = null ;
int returnValue;
public DBAccess() {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.out.println("Driver class not found: " +
e);
}
try {
con =
DriverManager.getConnection(url, userid, pw);
stmt = con.createStatement();
} catch (SQLException e) {
System.out.println("No connection: " + e);
}
}
}
extends DBAccess
import java.sql.*;
private ResultSet rs;
private void doSelect(String select) {
try {
rs = stmt.executeQuery(select);
} catch (SQLException e) {
System.out.println("Problem with " + select + e);
}
}
This method will be the “utility” method that executes all possible selections in the Employee class. Clients will access other public methods (like getEmpNums) to retrieve Employee information. These public methods will create the correct SQL select statements and invoke doSelect. Because there is no reason for clients to use doSelect (or even know that it exists), doSelect is “hidden” by being defined as private. Notice also that the results are assigned to the class variable rs.
public ResultSet getEmpNums() {
String select =
new String("SELECT empnum FROM tntdb.employee ");
this .doSelect(select);
return rs;
}
The method getEmpNums defines the SQL statement that will retrieve all the employee numbers. The SQL statement is assigned to the String variable named select and select is passed to the doSelect method for execution. The doSelect method executes the SQL statement and assigns the results to rs. getEmpNums then returns rs to the calling method (i.e., the client).
package c10;
import java.awt.Choice;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.sql.*;
public class EmpNumFrame extends UsefulFrame
implements ItemListener {
private String empNum, txType;
private Choice EmpNumCh = null ;
private Employee emp = new Employee();
public EmpNumFrame(String tx) {
super ();
initialize();
txType = tx;
}
private void initialize() {
this .setSize(278, 125);
this .setLocation(450, 303);
this .setLayout( null );
this .add(getEmpNumCh(), null );
this .setTitle("Select Employee Number");
this .setVisible( true );
}
private Choice getEmpNumCh() {
if (EmpNumCh == null ) {
EmpNumCh = new Choice();
EmpNumCh.setBounds(85, 59, 108, 21);
EmpNumCh.addItemListener( this );
ResultSet rs = emp.getEmpNums();
try {
while (rs.next()) {
EmpNumCh.add(rs.getString(1));
}
} catch (SQLException e) {
System.out.println("No connection: " + e);
}
}
return EmpNumCh;
}
public void itemStateChanged(ItemEvent arg0) {
empNum = EmpNumCh.getSelectedItem();
System.out.println("We are getting ready to " +
txType + " emp num " + empNum);
this .dispose();
}
}
Notice that EmpNumFrame’s constructor is expecting a String variable. The function specified by the user on EmpOptions (update or display) must be passed to EmpNumFrame. This is because if display is selected, EmpNumFrame will create an EmployeeFrame object, but if update is specified, an EnterEmpInfo object will be created. Later we will modify the itemStateChanged method to do this. For now, itemStateChanged will help us test by simply displaying the employee number specified and the function.
In addition, notice that itemStateChanged deletes the EmpNumFrame object (the dispose statement does this). This means that after the user has specified the employee number, the frame will disappear.
insEmpCB Insert Employee Information
dispEmpCB Display Employee Information
updEmpCB Update Employee Information
if (insEmpCB.getState()) {
EnterEmpInfo eei = new EnterEmpInfo();}
else if (dispEmpCB.getState()){
EmpNumFrame enf = new EmpNumFrame ("display");}
else if (updEmpCB.getState()){
EmpNumFrame enf =
new EmpNumFrame ("update");}
Notice that EmpNumFrame is only created if the function display or update is chosen and that the function is passed to EmpNumFrame. If insert is selected, the EnterEmpInfo frame is displayed. Now we need to change AppOptions so that it will display EmpOptions.
if (empAppCB.getState()) {
EnterEmpInfo eei = new EnterEmpInfo();}
to:
if (empAppCB.getState()) {
EmpOptions eo = new EmpOptions();}
Time to test.
The Application Menu should be displayed as in Figure 10-10.
The Employee Application Menu should be displayed as in Figure 10-11.
The Select Employee Number frame should be displayed.
The Select Employee Number frame should disappear and the following should be displayed in the Console:
We are getting ready to update emp num 222
Tutorial: Finishing the Employee Application, Part 1
We must add the ability to insert and update to the Employee class and modify EnterEmpInfo to invoke the correct Employee methods based on the function selected. Remember, Employee will be performing all the database functions. EnterEmpInfo is simply the user interface to enter the information and specify what function (insert or update) to perform. In addition, we need to modify EnterEmpNum so that when the employee number is specified, an Employee object is created. This Employee object should retrieve the employee information from the database and populate the properties. EnterEmpNum will then pass the Employee object to EnterEmpInfo (for an update) or EmployeeFrame (for a display).
First, we will add the Employee methods to retrieve, insert, and update an employee’s information. Then we will modify EnterEmpNum and EnterEmpInfo to invoke the correct Employee methods.
private void getEmpInfoFromRS() {
try {rs.next();
this .setEmpNum(rs.getString(1));
this .setEmpName(rs.getString(2));
this .setEmpStreet(rs.getString(3));
this .setEmpCity(rs.getString(4));
this .setEmpState(rs.getString(5));
this .setEmpZip(rs.getString(6));
this .setPayRate(rs.getDouble(7));
this .setExemptions(rs.getInt(8));
} catch (SQLException e) {
System.out.println("Problem getting emp info from"
+ " result set " + e);
} catch (InvalidValue e) {
System.out.println("Bad pay rate value retrieved " + e);
}
}
This will set the Employee object’s properties to the information retrieved from the Employee table (which is in the result set rs). This is one of the private methods that the clients don’t need to know about. In other words, clients will never have to deal with SQL statements, database-related objects (result sets, connections, etc.), or methods (getString, getStatement, etc).
public void getEmpInfo(String empNum) {
String select = new String("SELECT * FROM " +
"tntdb.employee WHERE empnum = '" + empNum + "'");
this .doSelect(select);
this .getEmpInfoFromRS();
}
This method builds the select statement, invokes doSelect to retrieve the data, and then invokes getEmpInfoFromRS to set the object’s properties.
Now we will change EmpNumFrame to invoke getEmpInfo for the employee number (specified by the user) and then create an EmployeeFrame object to display the information.
emp.getEmpInfo(empNum);
if (txType.equals("display")) {
EmployeeFrame ef = new EmployeeFrame(emp);
}
Let’s test.
The Select Employee Number frame will disappear and the Employee Information frame will be displayed with the Joe Programmer information.
Well, display was fairly easy to implement.
For updates, we must change the EnterEmpInfo frame to receive the Employee object (from EmpNumFrame), display the properties, and then update the data in the Employee table.
For inserts, the EnterEmpInfo frame must create an Employee object and then issue the command to insert the data. Oh, and we have to create methods in Employee to insert and update the information in the table.
Tutorial: Finishing the Employee Application, Part 2
Because we have put the option to display employee information on the Employee Application Menu, the display button on EnterEmpInfo isn’t needed any more. What we really need is an update/insert button We will change the display button’s name to actionBtn and, depending on the function selected from the menu, set the button’s label to the appropriate text.
This will define the default button text as “Insert.” The application must change the button text to “Update” when the user has selected update as the function. If an Employee object is passed from EmpNumFrame, EnterEmpInfo knows that the function is update. So, we need to create a new constructor that
public EnterEmpInfo(Employee e) {
this ();
emp = e;
actionBtn.setLabel("Update");
}
That first statement probably looks funny. The first statement executes the class’s null constructor. Since all of the set-up functions still need to be performed, we simply run the null constructor instead of duplicating the code in the new constructor.
The second statement assigns the employee object (assigned to the method variable e) to the class variable emp (so other methods can access the object). And, of course, the last statement changes the button text.
Now we need to modify EmpNumFrame to send the Employee object when update is selected by the user.
if (txType.equals("update")) {
EnterEmpInfo eei = new EnterEmpInfo(emp);
}
We should be able to select a particular employee for update, and when the EnterEmpInfo frame is displayed, the button should have update as the text. Let’s test.
The EnterEmpInfo frame should be displayed, and the action button text should be Update. None of the employee information is displayed, because we still must insert the code to retrieve the employee information and populate the text fields. We will create a separate method called populateTFs to do this and have the constructor invoke the method.
private void populateTFs() {
empNumTF.setText(emp.getEmpNum());
empNameTF.setText(emp.getEmpName());
empStreetTF.setText(emp.getEmpStreet());
cityTF.setText(emp.getEmpCity());
stateTF.setText(emp.getEmpState());
zipTF.setText(emp.getEmpZip());
empPRTF.setText(String.valueOf(emp.getPayRate()));
exmpChoice.select(String.valueOf(emp.getExemptions()));
}
this .populateTFs();
The Mary Worker information should be displayed in the text fields.
We will now add the insert and update functions to Employee, and then modify EnterEmpInfo to invoke those functions
private void doUpdate(String update) {
try {
stmt.executeUpdate(update);
} catch (SQLException e) {
System.out.println("Problem with " + update + e);
}
}
public void doInsert() {
String insert = "INSERT INTO tntdb.employee " +
createValuesClause();
doUpdate(insert);
}
public void doUpdate() {
String update = "UPDATE tntdb.employee " +
createSetClause() + createWhereClause();
doUpdate(update);
}
public void doDelete() {
String delete = "DELETE FROM tntdb.employee " +
createWhereClause();
doUpdate(delete);
}
private String createValuesClause() {
String values = "VALUES ('" +
empNum + "', '" +
empName + "', '" +
empStreet + "', '" +
empCity + "', '" +
empState + "', '" +
empZip + "', " +
payRate + ", " +
exemptions + ") ";
return values;
}
private String createSetClause() {
String set = "SET " +
"empNum = '" + empNum + "', " +
"empName = '" + empName + "', " +
"street = '" + empStreet + "', " +
"city = '" + empCity + "', " +
"state = '" + empState + "', " +
"zip = '" + empZip + "', " +
"payRate = " + payRate + ", " +
"exempts = " + exemptions + " ";
return set;
}
private String createWhereClause() {
String where = "WHERE empNum = '" + empNum+ "' ";
return where;
}
Notice that only three of the new methods are public. These public methods are the interface that clients of the Employee class use to perform the insert, update, and delete functions. Data encapsulation dictates that the encapsulating class should provide public CRUD(Create, Read, Update, and Delete) functions for other classes to access the data. The getEmpInfo method provides the read function, and the constructors provide the create function. We just added the doInsert, doUpdate, and doDelete methods to provide the UD in CRUD.
In actuality, the three public classes simply build the SQL statement string and then pass that string to the private methods doSelect(String) or doUpdate(String) for execution. Looking even closer, you will see that we have split the creation of the set, values, and where clauses into separate methods. Because updates, deletes, inserts, and selects may use the same clauses, we created separate methods so we only define the clauses once. Notice that the createXxxx methods do not use the public getters and setters to access the property values, they access the private variables directly. Because the methods are private, there is no danger in modifying the private variables directly. We will now enable the Insert and Update functions in EnterEmpInfo.
emp.setEmpNum(empNumTF.getText());
if (actionBtn == e.getSource()) {
EmployeeFrame ef = new EmployeeFrame(emp);
to:
if (actionBtn == e.getSource()) {
if (actionBtn.getLabel().equals("Insert")) {
emp.doInsert();
}
else { if (actionBtn.getLabel().equals("Update")) {
emp.doUpdate();
}
}
this .dispose();
Time to test.
333
Anne Analyst
3 Logic La
Jacksonville
FL 32233
22
1
The Anne Analyst information should be displayed in the Employee Information frame.
The Anne Analyst information will be shown on the Enter Employee Information frame.
The Employee Information frame will be displayed with the Anne Analyst information and the new value of 4 Logic La for the street address.
Yay, it works!! However, there are several shortcomings with our application. For instance, deleting an employee’s information is not an option on any of the frames, nor are there any checks to stop duplicate employee information from being inserted into the database. This could have been easily solved by adding the delete option to the Employee Options frame, coding the call to Employee’s doDelete method, and doing a simple check before inserting the new employee information. Also, navigating the frames to access the various functions is awkward. Certainly, a Web page with links to all the functions (on the left side of the page) would be much easier. We also have not been very careful about closing the connection—which wastes system resources. Finally, the way we create and execute the SQL commands (dynamically) is very inefficient, especially for high-volume database applications. Using PreparedStatement objectsinstead of Statement objects is more efficient. However, adding code to overcome these shortcomings would have added to the complexity of the application and taken the focus away from the point of the chapter, which was how to access a database.
Data Access in a Complex Environment
Most large organizations have multiple DBMSs and often many different DBMS types. In these complex environments, tables are often moved between the DBMSs, database locations are changed, and there are constant changes to the structure of tables (i.e., adding and deleting fields, changing field sizes, etc.) These types of changes will require changes to the Java classes. Programmers can minimize the coding needed to implement these changes by using super classes and inheritance to eliminate duplicate code. In addition, abstract classes should be used to enforce standards across the different DBMS types.
As an example, we’ll assume an organization has one installation of each DBMS type (DB2, Oracle, and Access). To support the DBMSs, a “DBAccess-like” class should be created for each DBMS. This means the programmer would create three Java classes (called DB2DB, OracleDB, and AccessDB) with properties to hold the unique information required to access that particular DBMS. (If the three DBMSs were of the same type, you could put the IP address or URLs of the different hosts in the Java class name (e.g., OracleDB111. 222.333.4) to distinguish between them.
The smart programmer will also implement a common interface for all the DBMSs. Our example will consist of public methods called doInsert, doUpdate, and doDelete. Regardless of the DBMS type, internally, each class should work the same way. So, we will force each class to have non-public methods called createValuesClause, createSetClause, createWhereClause, getFieldsFromRS, doSelect(String), and doUpdate(String). A superclass (called DB) will be created that defines each of these required methods as abstract. In addition, DB will have a method (called init) that creates all the needed DB objects (connection, statement, etc.) and common variables that the table classes need, such as a ResultSet variable called rs, a Statement variable called stmt, and so on.
The code for DB would be as follows:
package c10;
import java.util.*;
import java.sql.*;
abstract class DB {
Connection con = null ;
String url = null ;
String user, pw;
Properties p = new Properties();
String driver = null ;
public boolean conExists = false ;
ResultSet rs = null ;
Statement stmt = null ;
int returnValue;
public void init() {
if (conExists == false ) {
try {
Class. forName (driver);
p.put("naming", "sql");
p.put("user", user);
p.put("password", pw);
con = DriverManager. getConnection (url, p);
stmt = con.createStatement();
} catch (ClassNotFoundException e) {
System. out .println("couldn't find driver");
e.printStackTrace();
System. exit (1);
} catch (SQLException e) {
System. out .println("couldn't connect");
e.printStackTrace();
System. exit (1);
}
}
conExists = true ;
}
public abstract void doInsert();
public abstract void doUpdate();
public abstract void doDelete();
abstract String createValuesClause();
abstract String createSetClause();
abstract String createWhereClause();
abstract void getFieldsFromRS();
abstract void doSelect(String string);
abstract void doUpdate(String string);
}
Notice that before creating a connection object the init method checks to see if a connection object already exists. This will cut down on the overhead of creating and maintaining multiple connections. Of course, each individual “table class” will have to invoke the init method to ensure that the connection is created at least once.
Notice also that the init method uses a Properties object. Instead of dealing with all the individual property variables, a Properties object is defined to hold many of the values and then passed to the DriverManager. Lastly, note all the abstract methods. The various “table” subclasses of DB will be required to define these methods.
We need the “database classes” AccessDB, DB2DB, and OracleDB. As mentioned, each will contain the information unique to that DBMS. The source code for each is:
package c10;
abstract class AccessDB extends DB{
public AccessDB () {
driver = "sun.jdbc.odbc.JdbcOdbcDriver";
url = "jdbc:odbc:TNT Database";
user = "anonymous";
pw = "guest";
}
}
package c10;
abstract class DB2DB extends DB{
public DB2DB () {
driver = "com.ibm.as400.access.AS400JDBCDriver";
url = "jdbc:as400:111.22.333.444";
user = "bjanson";
pw = "jeter";
}
}
package c10;
abstract class OracleDB extends DB {
public OracleDB () {
driver = "oracle.jdbc.driver.OracleDriver";
url = "jdbc:oracle:thin:@111.22.333.444:1521:SID";
user = "bjanson";
pw = "jeter";
}
}
Notice that each of the DB classes is defined as abstract and extends DB. Because they are abstract, they do not have to implement the abstract methods defined in DB. However, all of the abstract methods will have to be defined in Employee and the ones that are currently defined as private will have to have the access modifier removed. Because DB defines the methods as protected, Employee can’t define them more restrictively as private. Speaking of changes to Employee, Employee must call the init method (to establish the connection to the database) and we need to implement a getFieldsFromRS method. We could rename getEmpInfo-FromRS to getFieldsFromRS, however, it’s simpler to code getFieldsFromRS to invoke get-EmpInfoFromRS. Finally, of course, you would substitute all your connection information for the example information above.
In Employee, we would change the constructor to invoke init as follows:
public Employee(){
init();
}
If there are multiple constructors, all must invoke the default constructor. In Employee, there is a constructor that accepts five strings. It would be modified to the following:
public Employee(String name, String street, String
city, String state, String zip) {
this ();
this .setEmpName(name);
this .setEmpStreet(street);
this .setEmpCity(city);
this .setEmpState(state);
this .setEmpZip(zip);
}
As mentioned, each of the private methods headers must be changed as follows:
String createValuesClause() {...
String createSetClause() {...
String createWhereClause() {...
void doSelect(String select) {...
void doUpdate(String update) {..
and getFieldsFromRS can be coded as follows:
void getFieldsFromRS() {
getEmpInfoFromRS();
}
Assuming Employee is currently in the DB2 database, the Employee class header would look like the following:
public class Employee extends DB2DB{...
and the class diagram would look like Figure 10-13. Now comes the magic of inheritance.
If the Employee table is moved to the Oracle DBMS, all a Java programmer would have to change is the Employee header to extend the OracleDB class as follows:
public class Employee extends OracleDB{...
And, as you probably guessed, if the Employee table were moved to the Access DBMS, the Employee header would be changed to:
public class Employee extends AccessDB{...
Because we specified the DBMS information in separate classes (e.g., OracleDB, AccessDB) rather than in the “table classes” (e.g., Employee) and built a superclass/subclass relationships (so that the required objects, methods, and variables are inherited), physical changes to table locations have a minimal impact on the Java classes. In addition, if the DBMS was moved to a different host or a password was changed, only the DBMS classes (containing the URL address and password) would need to be changed. Even if the Java technology for accessing a database changed (e.g., connection objects were replaced), only the DB class’ init method would be affected.
A great deal of the advances in Java (Java Server Faces (JSFs), Enterprise Java Beans (EJBs), etc.) are in the area of working with databases. In the next chapters, we will examine some of these new technologies.
Let’s look at the results:
DBAccess EmpNumFrame EmpOptions
AppOptions Employee EmployeeFrame
EnterEmpInfo ExitButton TNT
TNTButton UsefulFrame
Review Questions
You will modify the Shipment application to insert, update, display, and delete shipment information in a DBMS. In addition, the Enter Shipment Information frame will offer the capability to delete and update shipment information.
To perform all this, the ShipOptions class must be modified to pass the selected function to EnterShipNum. EnterShipNum will receive the shipment number entered by the user and create a Shipment object for that shipment number. EnterShipNum will pass the selected function and the Shipment object to EnterShipInfo. EnterShipInfo will display the correct button text (insert, delete, etc.). The Shipment class will accept a shipment number, access the database for that shipment’s information, and populate the Shipment object’s properties with the database information. Shipment will also be modified to provide all the CRUD functions.
AppOptions DBAccess Employee
EmpNumFrame EmpOptions EnterEmpInfo
updShipCB.addItemListener( this );
dltShipCB.addItemListener( this );
else if (shipAppCB.getState())
System.out.println("Sorry that application is not " + "available");
to:
else if (shipAppCB.getState()){
ShipOptions so = new ShipOptions();}
This means that the Shipment Options frame will be displayed when that option is selected from the Application Options frame
public Shipment(String sNum) {
}
The code should look like the following:
public EnterShipInfo(Shipment ship, String func) {
this ();
}
By accepting a Shipment object, EnterShipInfo will be easier for other classes to use.
private String function;
Since we will change EnterShipNum to perform deletes and updates, the user selected function needs to be passed to EnterShipNum and stored in this class level variable.
The source code should look like the following:
public EnterShipNum(String func) {
super ();
initialize();
function = func;
}
The code should look like the following:
public void actionPerformed(ActionEvent e) {
String sNum = shipNumTF.getText();
Shipment shipment = new Shipment(sNum);
if (function.equals("Display")) {
new ShipmentFrame(shipment);
} else {
new EnterShipInfo(shipment, function);
}
this .dispose();
}
Notice that neither the EnterShipInfo nor the ShipmentFrame objects are assigned to a variable. Because we will never reference the objects in the source code, there is no need to assign variables to them.
private String function;
To do this, change the source to the following:
if (dispShipCB.getState())
function = "Display";
else {
if (updShipCB.getState()) {
function = "Update";
} else {
if (dltShipCB.getState()) {
function = "Delete";
}
}
}
if (enterShipCB.getState()) {
EnterShipInfo esi = new EnterShipInfo();
} else {
EnterShipNum esn = new EnterShipNum(function);
}
So far the steps you have performed have defined “the plumbing” necessary for the classes to communicate correctly. The next steps will add the CRUD methods to Shipment and change EnterShipInfo and EnterShipNum to invoke the appropriate Shipment methods. We assume that the Shipment table was created in TNTDB with the following SQL command:
CREATE TABLE tntdb.shipment
(empnum CHAR(10),
shipnum CHAR(10),
rcvmon CHAR(2),
rcvday CHAR(2),
rcvyear CHAR(4),
rcvhour CHAR(2),
rcvmin CHAR(2),
rcvampm CHAR(2),
supplier CHAR(20))
rcvMon rcvDay rcvYear
rcvHour rcvMin rcvAMPM
public Shipment(String sNum) {
this .setShipmentNum(sNum);
this .getShipInfo(sNum);
}
public Shipment ship;
to :
private Shipment ship = new Shipment();
and, in the initialize, method comment out the following:
ship = new Shipment("","","","","");
public EnterShipInfo(Shipment ship, String func) {
this ();
this .ship = ship;
populateTFs();
actionBtn.setLabel(func);
}
Results of the Review Exercise
Below are the results:
Check that the Exercise Was Done Correctly
Let’s check if everything is working:
Notice that the message text does not fit in the labels and there are extra spaces following the shipment number. These problems will be addressed in the Challenge Exercise.
Problem getting ship info from result set java.sql.SQLException: Xxxxx xxxxx.
Because shipment 1000 was deleted, the result set returned by the select is empty. When Shipment tries to read the result set (to put the values in the properties), we should check to see if the result set is empty. This is another deficiency the Challenge Exercise will correct.
Finally, let’s try a challenge:
Shipment number xxxxx does not exist in the database.
There are several reasons the shipment information is not displayed correctly:
(In other words, if the month value is 4, the retrieved value will be “4 ” - the character 4 followed by a blank space. This results in the spacing being “off”. In addition, because of the blank space, trying to parse “4” will result in a number format exception being thrown.)
Check that the Exercise Was Done Correctly
Perform the same tests as were done for the Review Exercise and confirm that the problems have been resolved.
3.145.87.161