Think of a "graffiti wall" as a place where people jot down open-ended thoughts, until someone comes and erases those thoughts, possibly in order to make space to write new thoughts. Well, we at Leverage thought that this might be an interesting demo application to provide to our clients. In the Locomotive, the Graffiti Wall is a place where people visit a certain location (a HTML template) where they can view everything that other people have written. They can also add something new to that location; there is a text field on the Graffiti Wall page to allow them to do that. There are 10 spaces available for people to write their thoughts. Whenever someone submits a new thought, which we call a "Tag", that new Tag will overwrite the oldest Tag that is on the wall. Any user, including anonymous users, can submit new Tags, and any user can view the Graffiti Wall.
For our purposes here, the Graffiti Wall is also an excellent example of a simple application which interacts with the database. In this example, we will use some SQL statements and some JDBC method calls. If you don't know SQL, we suggest that you learn it before writing code for the Locomotive. (Don't worry, it's not very hard. You can probably learn something about it just from reading this example.) We'll be looking at the structure of the table in the database which stores Tags, as well as the Java code to store and read Tags from the database.
When a user comes to the Graffiti Wall, the GraffitiWallHandler will ask the Tag class to load all the Tags from the database, via an SQL "select..." statement, and display them within an HTML Table. Each row of the HTML Table will display one Tag. When a user submits a new Tag, that Tag will be saved to the database using an SQL "update..." command. That command will simply replace the oldest Tag with the Tag that the user has just submitted.
And that's all there is to it. See, I told you. First, we're going to look at how Tags are stored in the database. Because we have already discussed how Handlers and GenericHandlers interact, we'll let you read the code for GraffitiWallHandler on your own.
In English, what this means is that we will create a new entity in the database, a new kind of object: a table called "loco_graffiti_tags", which has four fields, or "columns" in database lingo. The first column, tag_id, is the PRIMARY KEY of this table, which is used to uniquely identify each entry, or "row", in the loco_graffiti_tags table. Every row must have the PRIMARY KEY field filled in -- it cannot be empty. The PRIMARY KEY is always "indexed" for fast access. The second column, tag_author, stores the username of the user who submitted this Tag. The third column, tag_text, stores the text of the Tag. The fourth column, ins_time, stores the time and date when this Tag was written into the database. A field of type varchar2 (32) contains text that can have variable length, from being empty to 32 characters. varchar2 fields only take up as much space as they need, so they're efficient.create table loco_graffiti_tags ( tag_id number (20) PRIMARY KEY, tag_author varchar2 (32), tag_text varchar2 (2000), ins_time date ) tablespace loco_app_ts;
To learn about what tablespaces are, consult your database manual.
The purpose of this method is to read every Tag into memory, and collect them all into an array of Tags. It will issue an SQL query to the database and then read the results, one row at a time, putting them into Tag objects./** * loads in all rows from the db, sorted by date in descending order. * if there are no rows in the table, an array of zero length * (Tag[0]) is returned. */ public static Tag[] loadAllTags (Connection conn) throws SQLException { String statement = "select tag_author, ins_time, tag_text from " + "loco_graffiti_tags order by ins_time desc"; Statement stmt = null; ResultSet rs = null; Vector v = new Vector(); try { stmt = conn.createStatement(); rs = stmt.executeQuery (statement); while (rs.next()) { // uses private constructor v.addElement (new Tag (rs.getString (1), rs.getTimestamp (2), rs.getString (3))); } } finally { if (rs != null) rs.close(); if (stmt != null) stmt.close(); } Tag[] tags = new Tag[v.size()]; v.copyInto (tags); return (tags); }
First, let's look at the SQL query statement:
When translated into English, the "select..." statement means: "for every row (or 'entry' or 'record') of the table called loco_graffiti_tags, give me the columns (or 'fields') named tag_author, ins_time, and tag_text. Also, return these results sorted by descending ins_time." Of course, SQL is much more concise. You send this SQL command to the database by using the following two methods:String statement = "select tag_author, ins_time, tag_text from " + "loco_graffiti_tags order by ins_time desc";
Statement and ResultSet are classes in the java.SQL package. You can refer to the Java class documentation to read about them. Roughly, an instance of Statement class represents a single SQL statement to be sent to the database to be executed. An instance of ResultSet class represents the results, which are a list of rows, that are returned by a query to the database. After "rs = stmt.executeQuery (statement)" is called, the variable "rs" will be associated with a database "cursor" which points to a list of rows. Before you can access each row, you must call the method rs.next() (even before the first row) to make the database cursor point to that row:Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery (statement);
And then, all you need to do to read each column of the current row is to call one of the get...() methods of ResultSet, e.g. getString(). The 1 in getTimestamp(1) means "the value in column 1, which must be a Timestamp", and refers to the first column specified in the "select..." statement, which was "time_submitted". Note that you should always use the class Timestamp from the package java.SQL to represent dates from the database. java.SQL.Date and java.util.Date are NOT compatible with JDBC dates!while (rs.next()) { // uses private constructor v.addElement (new Tag (rs.getString (1), rs.getTimestamp (2), rs.getString (3))); }
Also, always remember to close ResultSets and Statements. It's better protocol. If you don't close them, it might affect your database Connections. Notice that the following code is enclosed within a finally { } clause:rs.close(); stmt.close();
This is because we want to close the ResultSet and Statement we used, no matter what, even if there were an SQLException or some other problem. If we forget to close our ResultSets, we may eventually run out of database cursors. Here's why. In the Locomotive, we don't create a new Connection for every Handler that is executed. To save overhead, we re-use Connections by placing them into something called a DBConnectionPool. We withdraw a Connection from this pool when a Handler starts, and after the Handler finishes, we return the Connection back to this pool.finally { if (rs != null) rs.close(); if (stmt != null) stmt.close(); }
Now, because Connections are never closed, any ResultSets associated with them which have not been closed will stay around -- and since the database associates a cursor with each ResultSet, those cursors stay around as well. Well, databases usually allocate a limited number of cursors, and, once all the cursors are used up, no more database clients (such as the Locomotive) can connect to the database. So, remember to close your ResultSets and Statements!!!
Okay, now back to our main program, the source code. Once we've read all the Tags from the database and stored them into the Vector "v", that we take all the information inside "v" and put it into an array of Tags:
(You know, Vector should really have a method called toArray().)Tag[] tags = new Tag[v.size()]; v.copyInto (tags); return (tags);
If you've ever written any code to display HTML Rows before, this method will seem familiar to you. <TR> means "start a row", and </TR> means "end this row". <TD> means "start a column in the current row", and </TD> ends that column. The code above reads all the Tags from the database, using the method "Tag.loadAllTags (conn)", and then, it formats each Tag as an HTML row. In this row, the first column displays the exact time and date that this Tag was submitted. The second column displays the username who submitted it, which might be "anonymous". The third column is the actual text submitted. All the rows are concatenated together into one big String, which is returned. This String will be put into a Steam variable called TAG_ROWS, which is inserted into a HTML template (in templates/examples/graffiti/graffiti.stm), where it will be enclosed by an HTML Table:private String getTagsAsHTMLRows() throws SQLException { StringBuffer html_rows = new StringBuffer(); int index = 0; Tag[] tags = Tag.loadAllTags (conn); // the following will allow us to format a date to look like: // Feb 14 at 8:45:00 PM SimpleDateFormat timeFmt = new SimpleDateFormat ("MMM d 'at' h:mm:ss a"); for (int index = 0; index < tags.length; index++) { if (tags[index].getText() != null) { html_rows.append("<TR>"); html_rows.append("<TD>"); html_rows.append(timeFmt.format(tags[index].getInsTime())); html_rows.append("</TD>"); html_rows.append("<TD>"); html_rows.append(tags[index].getAuthor()); html_rows.append("</TD>"); html_rows.append("<TD>"); html_rows.append(tags[index].getText()); html_rows.append("</TD></TR> \n"); } } return new html_rows.toString(); }
The reason why we don't also include the <TABLE>...</TABLE> HTML tags in TAG_ROWS is because you may want to give your HTML Table special formatting, such as a special color, spacing between cells, or borders. So, returning the HTML rows gives you more flexibility.<TABLE border="0"> [TAG_ROWS] </TABLE>
Then, within your HTML template, you simply say:/** * Alternate, MUCH easier way to return an HTML Table of Tags. */ public static displayTags (Connection conn) throws SQLException { String statement = "select ins_time, tag_author, tag_text from " + "loco_graffiti_tags order by ins_time desc"; Statement stmt = null; ResultSet rs = null; try { stmt = conn.createStatement(); rs = stmt.executeQuery (statement); // Create a SortableTable with the capacity for // "maxRows" rows and 3 columns. SortableTable table = new SortableTable (maxRows, 3); // load all the results from the query, in ResultSet rs, // into the table. table.load (rs, true); // add the table as a Steam variable. steam_vars.put ("TAG_TABLE", table); } finally { if (rs != null) rs.close(); if (stmt != null) stmt.close(); } }
The [table...] command iterates through every row in the SortableTable object named "TAG_TABLE", which is a Steam variable that was given a value in the Java code. For every row, it inserts all the text after TAG_TABLE until the end bracket, ']'. The columns INS_TIME, TAG_AUTHOR, and TAG_TEXT get their names from the names of the fields of your SQL query statement:<TABLE border="0"> [table TAG_TABLE <TR> <TD>[colname INS_TIME]</TD> <TD>[colname TAG_AUTHOR]</TD> <TD>[colname TAG_TEXT]</TD> </TR> ] </TABLE>
Isn't the [table] command cool??!! It rocks!"select ins_time, tag_author, tag_text from "...
And here's the code for this constructor method:new Tag(conn, authors_username, text);
In the previous SQL statement, notice how we used SQL.quoteString() around both the "author" and "text" variables. (The SQL class is part of the org.locomotive.util package.) That is because those string values may contain characters which SQL databases would interpret in a special way, such as '%' or '&' or the single-quote character itself, '.public Tag (Connection conn, String author, String text) throws SQLException { this.author = author; this.text = text; overwriteRow(conn); // might not be the exact same as the date from the DB, but // it will be close enough for government work. this.insTime = new Timestamp(System.currentTimeMillis()); } private void overwriteRow(Connection conn) throws SQLException { String statement = null; Statement stmt = null; switch (config.DB_TYPE) { case LOCOConfig.ORACLE: statement = "update loco_graffiti_tags " + "set tag_id = loco_tag_id_sq.nextval, " + " tag_author = " + SQL.quoteString (author) + ", tag_text = " + SQL.quoteString(text) + ", ins_time = SYSDATE where " + "tag_id = (select min(tag_id) from loco_graffiti_tags)"; break; case LOCOConfig.INFORMIX: statement = "execute procedure overwrite_graf_tag ( " + SQL.quoteString (author) + ", " + SQL.quoteString(text) + " )"; break; } try { stmt = conn.createStatement(); stmt.executeUpdate(statement); } finally { if (stmt != null) stmt.close(); } }
Let's talk about the last line of the previous SQL statement:
What this means exactly is, "find the Tag in the database with the smallest tag_id -- in other words, the oldest Tag." So, this "where..." subclause means that the "update..." statement will change only one row in the tags table, the row with the oldest Tag. Notice that tag_id is both a part of the "where..." clause, as well as one of the fields that will be saved in the "update..." clause. As a part of the "where..." clause, tag_id is referring to the tag_id field of a Tag to be found, while, as a part of the "update..." clause, tag_id refers to the new value to be saved." where tag_id = " + " (select min(tag_id) from loco_graffiti_tags)";
switch (config.DB_TYPE)Now, where does the "config" variable get its DB_TYPE value? Actually, it is set in the Locomotive configuration file, loco.conf. For Oracle, we add the following line in loco.conf:
DB_TYPE oracleFor Informix, we add instead:
DB_TYPE informixIn order to use this information, first we need to access the Locomotive's global configuration file. Instead of accessing the file directly, we access the configuration variables through a LOCOConfig object: (this line comes from the variable declarations section at the top of the Tag class)
Once we have this config object, then we can find out what kind of database we're using, by accessing config.DB_TYPE.private static LOCOConfig config = LOCO.getConfig();
For example, LOCO.getConfig().getString ("LOCO_TEMPLATE_ROOT_PATH") will return the file path of the directory that contains all HTML templates for this Locomotive.getString (String config_variable_name); getInt (String config_variable_name);
Not exactly. You see, we designed the Graffiti Wall system so that it would make sure, at the beginning, that there would be exactly 10 rows in the loco_graffiti_tags table, before any GraffitiWallHandler was ever executed. In fact, it is in the init() method of GraffitiWallHandler that we do this:
Why do we use this approach in the first place, rather than inserting a Tag into the database each time, and deleting Tags only when there are more than 10 of them? The reason is, one "update..." SQL statement is faster, simpler, and less code than an "insert..." statement followed by a "delete..." statement. Plus, the "delete..." statement has to be "smart" enough to detect when there are 10 Tags in the database, which is a tricky issue./** * The GraffitiWall architecture requires that a certain number of Tags * already exist in the database. This is because * we are using "update" to overwrite Tags, rather than * an "insert" and then a "delete". */ public boolean init(Log server_log) { Connection conn = RequestManager.getDBConnection(); // if there are no tags, we should populate the tags table. Tag.prepopulate (conn); }
Yet another issue is that if you must perform two SQL statements in order to accomplish one action, then that is more vulnerable to problems than using one SQL statement. For example, what if two different Java threads were trying to perform the same action at the same time? In this particular example, there may be no problems, but, since Java is a parallel programming environment, and since you can run multiple Locomotives on the same machine, you must always think about the pitfalls of parallel programming -- race conditions and deadlocking. You have to play out different scenarios; you have to "act like the computer". You need to think about synchronizing shared resources, both in your Java programs (using synchronized { }), and in your database transactions (by trying to accomplish single actions with single SQL statements, or, if you can't, by using locks). We can't give a whole parallel programming class here, but please learn about it so that you will understand the issues.
Now, let's take a look at the code in Tag.prepopulate(). This method first checks whether there are 10 Tags already in the database. Then, if there are not enough, it will create enough Tags to bring the number up to 10.
In this code, we create enough new rows of Tags in the database to bring the total number of Tags to 10. For each new row, everything is the same; the only variation is a different tag_id. We set the value of the "tag_id" column to either "loco_tag_id_sq.nextval" (which retrieves the next value from a sequence) for Oracle, or the number "0" for Informix (Informix automatically returns an incremented ID when you specify a value of "0" for a Primary Key).public static prepopulate (Connection conn) throws SQLException { String statement = "select count(*), max(tag_id) from loco_graffiti_tags"; Statement stmt = null; ResultSet rs = null; PreparedStatement pstmt = null; try { stmt = conn.createStatement(); rs = stmt.executeQuery (statement); if (! rs.next()) { throw new SQLException ("count(*) loco_graffiti_tags failed!"); } int count = rs.getInt(1); int max_tag_id = rs.getInt(2); rs.close(); stmt.close(); if (count < maxRows) { statement = null; switch (config.DB_TYPE) { case config.ORACLE: statement = "insert into loco_graffiti_tags " + " (tag_id, tag_author, tag_text, ins_time) " + " values (loco_tag_id_sq.nextval, " + " 'anon', 'This is an example tag', ?)"; break; case config.INFORMIX: statement = "insert into loco_graffiti_tags " + " (tag_id, tag_author, tag_text, ins_time) " + " values (0, " + " 'anon', 'This is an example tag', ?)"; break; } pstmt = conn.prepareStatement(statement); Timestamp t = new Timestamp(System.currentTimeMillis()); for (int i=0; i < maxRows - count; i++) { pstmt.setTimestamp(1, t); pstmt.executeUpdate(); } } } finally { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (pstmt != null) pstmt.close(); } }
Notice that we can execute a PreparedStatement multiple times. Each time we execute it, we are setting the value for the '?' placeholder to "t". This is another major difference between a Statement and a PreparedStatement. For a Statement, if you want to change the value of one of the fields in the query and then re-execute the query, you need to re-create the entire SQL statement string; for a PreparedStatement, you only need to use a set...() method to set the value for that column, and then you can re-execute that query.
You might remember from a previous discussion that there is another way to specify the current date and time. You could simply substitute the '?' with the word SYSDATE for Oracle or CURRENT for Informix. In fact, we recommend that you use this approach, because it's faster for the database to set its own timestamps, and slower for us to create a value in Java and pass that value to the database. The reason why we used setTimestamp() in this example was to introduce the concept of setting values in PreparedStatements. On the other hand, whenever you need to deal with timestamps other than the current date and time, then you will definitely need to use PreparedStatements and setTimestamp().
But there is another reason why we use a PreparedStatement in this case. If you are executing multiple SQL statements, using a PreparedStatement may be more efficient for the database server, because it will parse the SQL statement and do the setup for it only once, and reuse the setup every time the PreparedStatement is executed again. However, for Statements, the database server has to go through the same amount of work parsing the SQL statement every time any single Statement is executed.
For your reference, here's a summary of situations where you should consider using PreparedStatements instead of Statements:
SUMMARY.
In this example, we looked at all the different aspects involved in writing Java code to interact with the database. First, we looked at the SQL statement to create the database table; then, we looked at the JDBC method calls that loaded and saved Tags to the database. We also discussed how you might customize your code based on whether your database was Oracle or Informix. Then, we talked about the differences between "update" statements, "insert" statements, and "delete" statements. Finally, we introduced PreparedStatements and compared them to regular Statements.