[ Team LiB ] |
4.2 A Guest Book ApplicationTo illustrate these most fundamental persistence concepts, I will use a simple Guest Book JSP application from my web site. You can see this example in action at http://george.reese.name/guestbook.jsp. The Guest Book enables visitors to a web site to leave comments and view the comments left by others. To prevent abuse, the application also includes an administrative approval mechanism. The full code for the Guest Book can be found on O'Reilly's FTP site. In accordance with the common persistence design patterns described earlier in the chapter, this application divides into view, control, business, data access, and data storage logic. Figure 4-1 is a UML class diagram illustrating this division. Figure 4-1. A UML class diagram for the Guest Book applicationThe view and control logic exist in two separate JSP pages. These JSP pages reference a Comment object containing the business logic. They are blissfully ignorant of any persistence logic—or of the existence of persistence at all. The Comment object, however, knows only that its data is persisted, but not how that data is persisted, because it delegates its data access through a CommentDAO data access object. I have chosen here to break down the data access even further, into individual objects supporting specific database operations. Without this trick, the data access object fills up with a jumble of SQL and JDBC that becomes difficult to manage. 4.2.1 The ViewThe view is a JSP page that displays a form and then lists all approved comments. Example 4-1 contains the code for this JSP. Example 4-1. A JSP view that lists comments and accepts new ones<%@ page info="Guest Book Form" %> <%@ page import="java.util.ArrayList" %> <%@ page import="org.dasein.gb.Comment" %> <%@ taglib uri="/WEB-INF/tlds/dasein.tld" prefix="dasein" %> <jsp:useBean id="user" scope="session" class="org.dasein.security.User"/> <% pageContext.setAttribute("user", user); %> <% String d = request.getParameter("done"); %> <% boolean done = ((d= =null) ? false : d.trim( ).equalsIgnoreCase("true")); %> <% String email, name, comment; %> <% email = request.getParameter(Comment.EMAIL); %> <% name = request.getParameter(Comment.NAME); %> <% comment = request.getParameter(Comment.COMMENT); %> <% if( user != null ) { %> <% String fn = user.getFirstName( ); %> <% String ln = user.getLastName( ); %> <% name = ((fn = = null) ? "" : (fn + " ")) + ((ln = = null) ? "" : ln); %> <% email = user.getEmail( ); %> <% } %> <% if( email = = null ) { %> <% email = ""; %> <% } else { %> <% email = email.trim( ); %> <% } %> <% if( name = = null ) { %> <% name = ""; %> <% } else { %> <% name = name.trim( ); %> <% } %> <% if( comment = = null ) { %> <% comment = ""; %> <% } else { %> <% comment = comment.trim( ); %> <% } %> <% String err = request.getParameter("errorID"); %> <% if( err != null ) { %> <% err = err.trim( ); %> <% if( err.length( ) < 1 ) { %> <% err = null; %> <% } %> <% } %> <% if( err != null ) { %> <dasein:printError/> <% } else if( done ) { %> <p class="text"> Thank you for your comment! I will review the comment. Assuming you did nothing offensive, it will appear below after I review it. </p> <% } %> <% if( !done ) { %> <p class="text"> <form method="POST" action="guestbook-action.jsp"> <label class="text" for="<%=Comment.NAME%>">Name:</label> <input id="<%=Comment.NAME%>" type="text" name="<%=Comment.NAME%>" value="<%=name%>" size="25"/> <br/><br/> <label class="text" for="<%=Comment.EMAIL%>">Email:</label> <input id="<%=Comment.EMAIL%>" type="text" name="<%=Comment.EMAIL%>" value="<%=email%>" size="25"/> <br/><br/> <label class="text" for="<%=Comment.COMMENT%>">Comments:</label> <br/> <textarea id="<%=Comment.COMMENT%>" name="<%=Comment.COMMENT%>" wrap="virtual" rows="10" cols="60"/><dasein:clean><%=comment%></dasein:clean></textarea> <br/> <input type="submit" value="Submit"/> </form> </p> <% } %> <h3 class="section">Comments</h3> <dl class="guestbook"> <% ArrayList cmts = Comment.getApproved( ); %> <% pageContext.setAttribute("cmts", cmts); %> <dasein:foreach id="cmt" source="cmts" className="org.dasein.gb.Comment"> <dt>On <%= cmt.getCreated( ) %>, <%=cmt.getName( )%> wrote:</dt> <dd><%=cmt.getComment( )%></dd> </dasein:foreach> </dl> The first part of this example pulls CGI (Common Gateway Interface) parameters into Java variables. It is specifically looking for all of the form fields as well as a done parameter and an errorID parameter. As we will see in the controller, whenever an error occurs, it sets the errorID parameter and redisplays the view. If any field values are passed in, it uses those values as default values for the form. On success, it will redisplay the list of comments—minus the form. After the initial parameter parsing logic, it displays a form unless the done parameter was set. Finally, the page uses a tag library containing a looping construct in the form of the dasein:foreach tag. For each comment it pulls from the Comment.getApproved( ) call, it displays the data from the comment. 4.2.2 The ControllerThe form from the view posts to the controller page. Example 4-2 shows this simple code. Example 4-2. The Guest Book controller that handles new comments<%@ page info="Guest Book Action" %> <%@ taglib uri="/WEB-INF/tlds/dasein.tld" prefix="dasein" %> <%@ taglib uri="/WEB-INF/tlds/guestbook.tld" prefix="guestbook" %> <%@ page import="org.dasein.jsp.Log" %> <guestbook:addComment error="error"> <% response.sendRedirect("guestbook-form.jsp?done=true"); %> </guestbook:addComment> <dasein:isNull name="error"> <dasein:when state="false"> <jsp:include page="guestbook-form.jsp"> <jsp:param name="errorID" value="<%=Log.storeException(error)%>"/> </jsp:include> </dasein:when> </dasein:isNull> The complexity of this action controller is hidden inside a couple of tag libraries. The first is the guestbook:addComment tag. It triggers the action of adding a new comment to the database. On success, the body of the comment is executed. In this case, the body of the comment redirects to the view page with the done parameter set.
The special tag dasein:isNull will execute the body of the tag if the specified value—in this case, error—is a null value. In this page, error will be null only if an error occurred while attempting to add a comment. It therefore stores the error message for later retrieval and displays the view page again so that the user may correct the error. As you can see from this simple page, a controller does not do much in and of itself. It simply acts as a traffic cop, determining what should actually happen in response to a user action. In this case, it triggers an event in the business object through a tag library. The code in the tag library is shown in Example 4-3. Example 4-3. A custom tag to trigger business logicpublic int doStartTag( ) throws JspException { try { ServletRequest request = pageContext.getRequest( ); String name = request.getParameter(Comment.NAME); String email = request.getParameter(Comment.EMAIL); String comment = request.getParameter(Comment.COMMENT); HashMap data = new HashMap( ); Comment cmt; if( name != null ) { name = name.trim( ); if( name.length( ) < 1 ) { name = null; } } if( name = = null ) { if( error != null ) { pageContext.setAttribute(error, NO_NAME); error = null; return SKIP_BODY; } else { throw new JspException(NO_NAME); } } data.put(Comment.NAME, name); if( email != null ) { email = email.trim( ); if( email.length( ) < 1 ) { email = null; } } data.put(Comment.EMAIL, email); if( comment != null ) { comment = comment.trim( ); if( comment.length( ) < 1 ) { comment = null; } } if( comment = = null ) { if( error != null ) { pageContext.setAttribute(error, NO_COMMENT); error = null; return SKIP_BODY; } else { throw new JspException(NO_COMMENT); } } data.put(Comment.COMMENT, comment); cmt = Comment.create(data); pageContext.setAttribute(error, null); return EVAL_BODY_TAG; } catch( PersistenceException e ) { if( error != null ) { pageContext.setAttribute(error, "<p class=\"error\">" + e.getMessage( ) +"</p>"); error = null; return SKIP_BODY; } else { throw new JspException(e.getMessage( )); } } } This tag library reads all of the form parameters and validates them. If they are not valid values, it sets an error value and ignores its body. Valid values are stuck in a HashMap that acts as a memento. This memento is then passed to the Comment.create( ) method to create a new comment in the database. 4.2.3 The Business Object (Model)Business objects form the heart of any major application. They model the underlying concepts of the application's problem domain. In the case of the Guest Book, the underlying concepts are users and the comments they leave behind. For simplicity's sake, we are not capturing users as objects in this system. In a more complex system, we probably would. The only business object being modeled here, then, is the Comment object. The guestbook-form.jsp view is, in short, a view of Comment objects. The Comment business object encapsulates everything there is about being a comment. It stores comment data captured in the comment forms and manages the creation, deletion, approval, and retrieval of comments. These operations have two elements:
Example 4-4 contains the meta-operations. Example 4-4. The metaoperations of the Comment business objectpackage org.dasein.gb; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import org.dasein.gb.persist.CommentDAO; import org.dasein.persist.PersistenceException; import org.dasein.persist.Sequencer; import org.dasein.util.Cache; public class Comment { static private final Cache cache = new Cache( ); static public final String APPROVED = "approved"; static public final String COMMENT = "comment"; static public final String COMMENT_ID = "commentID"; static public final String CREATED = "created"; static public final String EMAIL = "email"; static public final String NAME = "name"; static public Comment create(HashMap data) throws PersistenceException { Sequencer seq = Sequencer.getInstance(Comment.COMMENT_ID); Comment cmt; Long id; id = new Long(seq.next( )); data.put(Comment.COMMENT_ID, id); CommentDAO.create(data); cmt = new Comment(id, data); synchronized( cache ) { cache.cache(id, cmt); } return cmt; } static public ArrayList getApproved( ) throws PersistenceException { Iterator results = CommentDAO.getApproved( ).iterator( ); ArrayList cmts = new ArrayList( ); while( results.hasNext( ) ) { Long id = (Long)results.next( ); cmts.add(Comment.getComment(id.longValue( ))); } return cmts; } static public Comment getComment(long cid) throws PersistenceException { Long id = new Long(cid); synchronized( cache ) { Comment cmt = (Comment)cache.get(id); if( cmt = = null ) { HashMap data = CommentDAO.getComment(cid); data.put(Comment.COMMENT_ID, id); cmt = new Comment(id, data); cache.cache(id, cmt); } return cmt; } } static public ArrayList getPending( ) throws PersistenceException { Iterator results = CommentDAO.getPending( ).iterator( ); ArrayList cmts = new ArrayList( ); while( results.hasNext( ) ) { Long id = (Long)results.next( ); cmts.add(Comment.getComment(id.longValue( ))); } return cmts; } } In addition to representing a comment, the Comment class acts as a factory that contains four meta-operations:
The central data element for these operations is the comment cache stored in the static cache attribute. This cache uses the Cache class described earlier in the chapter. Whenever a comment is sought externally, this cache is checked first to see if the desired Comment instance has already been loaded. If not, the class will go to the data access object to load a new instance from the database. Otherwise, we can avoid a costly trip to the database and pull the object straight from the cache.
You probably also notice the constants defined at the top of the class. We saw them referenced earlier in the view page. It is simply a solid coding practice never to use literals in code. Instead, you should use constants like these to help avoid application bugs caused by spelling errors. private Boolean approved = null; private String comment = null; private Long commentID = null; private Date created = null; private String email = null; private String name = null; private Comment(Long cid, HashMap data) { super( ); commentID = cid; load(data); } public String getComment( ) { return comment; } public long getCommentID( ) { return commentID.longValue( ); } public Date getCreated( ) { return created; } public String getEmail( ) { return email; } public String getName( ) { return name; } public boolean isApproved( ) { return approved.booleanValue( ); } private void load(HashMap data) { approved = (Boolean)data.get(Comment.APPROVED); comment = (String)data.get(Comment.COMMENT); commentID = (Long)data.get(Comment.COMMENT_ID); created = (Date)data.get(Comment.CREATED); email = (String)data.get(Comment.EMAIL); name = (String)data.get(Comment.NAME); } public void remove( ) throws PersistenceException { HashMap data = new HashMap( ); data.put(Comment.COMMENT_ID, commentID); CommentDAO.remove(data); synchronized( cache ) { cache.release(commentID); } } public void save(HashMap data) throws PersistenceException { data.put(Comment.COMMENT_ID, commentID); CommentDAO.save(data); load(data); } } The instance operations are largely simple getter methods. The exceptions are the following:
The most critical thing to notice about the business object is that it hides all knowledge about persistence from the view and the controller. The view and controller simply do not need to know if the object persists or how it persists. In fact, the business object knows only that it persists—it knows nothing about how it persists. That knowledge is saved for the data access objects. 4.2.4 The Data Access ObjectsThe data access object, CommentDAO, provides a simple interface to the business object for persisting comments to the database. In short, it has methods to load, delete, update, and create comments. When the methods require data from the comment, the data is passed via a memento. They throw generic persistence exceptions. The data access object thus needs to know nothing about the internal structure of comments, and comments need to know nothing about the persistence details of the data access object. Example 4-5 contains the code for the CommentDAO class. Example 4-5. The CommentDAO data access objectpackage org.dasein.gb.persist; import java.util.ArrayList; import java.util.HashMap; import org.dasein.gb.Comment; import org.dasein.persist.Execution; import org.dasein.persist.PersistenceException; public abstract class CommentDAO { static public void create(HashMap data) throws PersistenceException { CreateComment.getInstance( ).execute(data); } static public ArrayList getApproved( ) throws PersistenceException { HashMap data = new HashMap( ); data.put(Comment.APPROVED, new Boolean(true)); data = ListComments.getInstance( ).execute(data); return (ArrayList)data.get(ListComments.COMMENTS); } static public HashMap getComment(long cid) throws PersistenceException { HashMap data = new HashMap( ); data.put(Comment.COMMENT_ID, new Long(cid)); data = LoadComment.getInstance( ).execute(data); return data; } static public ArrayList getPending( ) throws PersistenceException { HashMap data = new HashMap( ); data.put(Comment.APPROVED, new Boolean(false)); data = ListComments.getInstance( ).execute(data); return (ArrayList)data.get(ListComments.COMMENTS); } static public void save(HashMap data) throws PersistenceException { SaveComment.getInstance( ).execute(data); } static public void remove(HashMap data) throws PersistenceException { RemoveComment.getInstance( ).execute(data); } } This data access object further delegates to operation-specific objects to avoid clutter in this class. 4.2.4.1 Loading commentsThese delegates use the framework I described earlier in the chapter. Example 4-6 shows the LoadComment delegate that performs the SQL to load a comment from the database. Example 4-6. Loading a comment through a special delegatepackage org.dasein.gb.persist; import java.sql.SQLException; import java.util.HashMap; import org.dasein.gb.Comment; import org.dasein.persist.Execution; import org.dasein.persist.PersistenceException; public class LoadComment extends Execution { static public LoadComment getInstance( ) { return (LoadComment)Execution.getInstance(LoadComment.class); } static private final String LOAD = "SELECT approved, email, name, comment, created " + "FROM Comment " + "WHERE Comment.commentID = ?"; static private final int COMMENT_ID = 1; static private final int APPROVED = 1; static private final int EMAIL = 2; static private final int NAME = 3; static private final int COMMENT = 4; static private final int CREATED = 5; public HashMap run( ) throws PersistenceException, SQLException { long id = ((Long)data.get(Comment.COMMENT_ID)).longValue( ); HashMap res = new HashMap( ); String tmp; statement.setLong(COMMENT_ID, id); results = statement.executeQuery( ); if( !results.next( ) ) { throw new PersistenceException("No such comment: " + id); } tmp = results.getString(APPROVED); res.put(Comment.APPROVED, new Boolean(tmp.trim( ).equalsIgnoreCase("Y"))); tmp = results.getString(EMAIL); if( results.wasNull( ) ) { res.put(Comment.EMAIL, null); } else { res.put(Comment.EMAIL, tmp.trim( )); } res.put(Comment.NAME, results.getString(NAME)); res.put(Comment.COMMENT, results.getString(COMMENT)); res.put(Comment.CREATED, results.getDate(CREATED)); return res; } public String getDataSource( ) { return "jdbc/george"; } public String getStatement( ) { return LOAD; } } You should notice here again the liberal use of constants instead of literals throughout the code. This practice is very important in JDBC programming since the most efficient way to access columns in a result set is by column number.
The code executes a SQL SELECT and places the result into a memento. That memento goes back to the calling business object, which then sends it through the business object's load( ) method. If a JDBC error or some other exception occurs, the exception will be wrapped up in a PersistenceException and sent back to the calling business object. 4.2.4.2 Sequence generationThroughout this book, I reference the best practice of relying on your own, database-independent primary key generation mechanism. No discussion of the data access tier would be complete without a discussion of primary key generation. Every database engine provides a feature that enables applications to automatically generate values for identity columns. MySQL, for example, has the concept of AUTO_INCREMENT columns: CREATE TABLE Person ( personID BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, lastName VARCHAR(30) NOT NULL, firstName VARCHAR(25) NOT NULL ); When you insert a new person into this table, you omit the primary key columns: INSERT INTO Person ( lastName, firstName) VALUES ( 'Wittgenstein', 'Ludwig' ); MySQL will automatically generate the value for the personID column based on the highest current value. If one row exists in the database with a personID of 1, Ludwig Wittgenstein's personID will be 2. Some other databases have similar ways to generate primary keys; others provide wildly different tools. Reliance on your database engine's primary key generation tools has the following drawbacks:
I recommend the development of a database-independent primary key generation API that stores potential primary key values in the database. If you take this approach, you need to take care not to make too many trips to the database. You can avoid this pitfall by generating keys in memory and storing seed values in the database. The heart of this database-independent scheme is the following table: CREATE TABLE Sequencer ( name VARCHAR(20) NOT NULL, seed BIGINT UNSIGNED NOT NULL, lastUpdate BIGINT UNSIGNED NOT NULL, PRIMARY KEY ( name, lastUpdate ) ); The first time your application generates a key, it grabs the next seed from this table, increments the seed, and then uses that seed to generate keys until the seed is exhausted. Example 4-7 through Example 4-9 contain some of the code for a database-independent utility that handles unique number generation. It enables your application to simply use the following calls to create primary keys: Sequencer seq = Sequencer.getInstance("personID"); personID = seq.next( ); The tool guarantees that you will receive a value that is unique across all personID values. Example 4-7 contains the static elements that implement the singleton design pattern to hand out shared sequencers. Example 4-7. The code to serve up sequencerspublic class Sequencer { static private final long MAX_KEYS = 1000000L; static private final HashMap sequencers = new HashMap( ); static public final Sequencer getInstance(String name) { synchronized( sequencers ) { if( !sequencers.containsKey(name) ) { Sequencer seq = new Sequencer(name); sequencers.put(name, seq); return seq; } else { return (Sequencer)sequencers.get(name); } } } ... } The code provides two critical guarantees for sequence generation:
The attribute declarations and initialization for a sequencer define two attributes that correspond to values in the Sequencer table as well as a third attribute, sequence, to track the values handed out for the current seed, as shown in Example 4-8. Example 4-8. Setting up the sequencerprivate String name = null; private long seed = -1L; private long sequence = 0L; private Sequencer(String nom) { super( ); name = nom; } The core element of the sequencer—its public API—is the next( ) method. It contains the algorithm for generating unique numbers. The algorithm has the following process:
Example 4-9 contains the algorithm. Example 4-9. Generating a sequence IDpublic synchronized long next( ) throws PersistenceException { Connection conn = null; // when seed is -1 or the keys for this seed are exhausted, // get a new seed from the database if( (seed = = -1L) || ((sequence + 1) >= MAX_KEYS) ) { try { String dsn = System.getProperty(DSN_PROP, DEFAULT_DSN); InitialContext ctx = new InitialContext( ); DataSource ds = (DataSource)ctx.lookup(dsn); conn = ds.getConnection( ); reseed(conn); } catch( SQLException e ) { throw new PersistenceException(e); } catch( NamingException e ) { throw new PersistenceException(e); } finally { if( conn != null ) { try { conn.close( ); } catch( SQLException e ) { } } } } // up the sequence value for the next key sequence++; // the next key for this sequencer return ((seed * MAX_KEYS) + sequence); } The rest of the code is the database access that creates, retrieves, and updates seeds in the database. The next( ) method triggers a database call via the reseed( ) method when the seed ceases to be valid. The logic for reseeding the sequencer is fairly straightforward:
You can find the full code for the Sequencer class on O'Reilly's FTP site in the directory for this book. |
[ Team LiB ] |