Chapter 4. Persistence Fundamentals
Objects contain the possibility of all situations.
Persistence grants immortality to your business applications. Without it, you lose all of your application data every time the server shuts down. Database programming is the development of persistence mechanisms to save an application’s state to a relational database. In this book, I will cover a variety of persistence mechanisms, but this chapter introduces the basics through a custom guest book JSP application.
Patterns of Persistence
The excellent book Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley) has popularized the concept of design patterns. They are recurring forms in software development that you can capture at a low level and reuse across dissimilar applications. Within any application scope are problems you have encountered; patterns are the result of recognizing common problems and leveraging a common solution.
People have been writing database applications for nearly three decades. Over that time, many best practices have evolved into design patterns. As we explore different modes of persistence in this book, we will see many of these patterns over and over again.
Division of Labor
Perhaps the most essential element of good persistence design is a clear separation of application logic into the following areas:
- View logic
The view logic is responsible for displaying the user interface. It is the user’s window into the control and business logic.
- Control logic
The control logic handles user actions and decides what should happen based on those actions. It handles data validation and triggers the appropriate business logic on behalf of the user.
- Business logic
Business logic[4] encapsulates the basic business concepts behind your application. They provide the view with getter methods to access business data and provide the interface for creating, searching, modifying, and deleting the business objects they support.
- Data access logic
Data access logic maps business objects to the data storage layer. They are the heart of persistence.
- Data storage logic
The database engine provides you with this type of logic, which simply ensures your data is not lost at application shutdown.
Separation of logic with dependencies based on simple interfaces is a core principle of object-oriented software engineering. When you capture the essence of a business concept in a business object without burdening it with other logic, you enable it to be reused in other environments. For example, a bank account object that does not contain display or data access logic can be reused with JSP, Swing, and other kinds of frontends. It can also persist against different database engines.
Note
BEST PRACTICE: Divide application functionality into different logical components to facilitate component reuse.
This same principle extends beyond the business object layer. It also makes it easier to divide the work of building software among developers with different skills. With a good tag library, the view developer needs to know only XHTML and your tag library to write the view. The more difficult work of JDBC programming can be easily handed off to an experienced JDBC programmer without having to hand the entire application to a JDBC programmer.
Sequence Generation
In almost any database application, you need to generate unique identifiers to serve as primary keys in your database. Most databases have some sort of proprietary mechanism to help you generate sequences. Unfortunately, you cannot port a database application that relies on these proprietary schemes to other databases without changing the code that relies on those schemes.
I always recommend the use of a database-independent approach to sequence generation. Later in this chapter, I develop a sequence generator that will work for most database applications. It stores sequence seeds in the database. When an application needs a new unique number, it requests the unique number from the sequence generator. If the sequence generator has the seed in memory, it uses the following formula to create a new unique number:
unique = (seed * 1000000) + last; last++;
If the seed is not in memory, it is loaded from the database,
incremented, and the incremented value is stored back in the
database. When the seed runs out of unique values—when
last
reaches 1000000[5]—it loads a new seed from the
database, increments that new seed, and saves the incremented seed
back to the database.
This approach has several important features:
It generates unique values in a distributed environment. Multiple application servers can save business objects to multiple databases and still have the guarantee that the sequences being generated are unique across the entire system.
You do not need to go to the database every single time you generate a sequence. You go to the database only every million sequences.
The sequences are not tied to a specific table. You can create a sequence that is shared among several tables or even across the entire database. Similarly, you can have multiple values in the same table rely on different sequences.
Mementos
In the division of labor discussed earlier, the data access object needs to know about the state of the object it is persisting. You could pass the business object to the data access object, but doing so would require the data access object to know the intimate details of how the business object is implemented. The memento design pattern from the Design Patterns book comes to the rescue here.
A memento enables one object to share its state with another without either object needing to know anything about the other. Consider a common situation in which you have one class (class A) that references the values of another (class B). If you delete an attribute in class B, class A will no longer compile if it has direct references to the deleted attribute of class B. In general, this behavior is exactly what you want.
Sometimes—especially in mapping objects to a database—you want a looser coupling between two classes. The memento pattern creates this independence. It specifically enables you to make code changes to the business objects and data access objects independently of each other. A change to the business object will not require any changes to the data access object unless you are adding new data elements or removing obsolete ones. The data access object knows that the only changes it will care about come in the form of changes in the data contained in the memento. Similarly, any change to the underlying tables in the database is hidden from the business object. It always passes its state to the data access object and lets the data access object worry about persistence issues.
Object Caching
A database application must use the database as a persistent store—not as a memory store. In other words, you need to pull data from the database and hold it in memory in business objects. If you go to the database every time you want to display some data about a business object, your database application will perform terribly and fail to scale at all.
On the other hand, you don’t want to load the entire database in memory and keep it there. If you have a large amount of data, you will quickly run out of memory. It is therefore important to develop an object caching mechanism that strikes a solid balance between memory usage and database access.
In architectures like the EJB architecture, the application server
automatically manages caching for you. The Guest Book later in this
chapter, however, does not use EJBs. It therefore needs something
else to manage caching. It leverages a
Cache
class that uses a
SoftReference
to cache objects loaded from the
database.
Note
BEST PRACTICE: If you are building your own persistence system, implement an efficient caching scheme to prevent exhausting system resources.
A
SoftReference
is a
special kind of object in java.lang.ref
that
creates a
soft
reference to the object it stores. In Java, references
between objects are generally
strong references. For
example:
StringBuffer buffer = new StringBuffer( );
The reference to buffer
is a strong reference. The
strong reference is in force as long as the reference is in scope. If
the references fall out of scope, then the object is said to be no
longer strongly reachable. It is thus potentially available for
garbage collection.
A soft reference is a reference via a
SoftReference
object. By storing an object
indirectly through a SoftReference
instead of
directly, you make the object available for potential garbage
collection while still maintaining the ability to access the object
until it is garbage collected.
The
Cache
class implements
the Java Collection
interface. Internally, it even
uses a
HashMap
internally to
store data. When an application loads an object from the database, it
can put it in the cache using the
cache( )
method:
public void cache(Object key, Object val) { cache.put(key, new SoftReference(val)); }
This method creates a soft reference around the business object and
then stores the soft reference in the internal
HashMap
. As time goes by and the business object
is no longer in use, the soft reference will expire and the memory
the business object occupies will be freed. The code that checks for
the existence of a specific business object in the cache thus needs
to verify that the soft reference has not expired:
public boolean contains(Object ob) { Iterator it = cache.values( ).iterator( ); while( it.hasNext( ) ) { SoftReference ref = (SoftReference)it.next( ); Object item = ref.get( ); if( item != null && ob.equals(item) ) { return true; } } return false; }
The get( )
method has to perform similar
checks:
public Object get(Object key) { SoftReference ref = (SoftReference)cache.get(key); Object ob; if( ref = = null ) { return null; } ob = ref.get( ); if( ob = = null ) { release(key); } return ob; }
A Guest Book Application
To 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.
The 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.
The View
The view is a JSP page that displays a form and then lists all approved comments. Example 4-1 contains the code for this JSP.
<%@ 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.
The Controller
The form from the view posts to the controller page. Example 4-2 shows this simple code.
<%@ 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.
public 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.
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:
Metaoperations such as creation and retrieval of comments via static methods
Object-specific operations via instance methods
Example 4-4 contains the meta-operations.
package 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.
The Data Access Objects
The 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.
package 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.
Loading comments
These 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.
package 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.
Note
BEST PRACTICE: Access JDBC columns by number and use constants to keep those column values readable and maintainable.
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.
Sequence generation
Throughout 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:
Every database engine handles key generation differently. It is thus difficult to build a truly portable JDBC application that uses proprietary key generation schemes.
Until JDBC 3.0, a Java application had no clear way of finding out what keys were generated on an insert.
In many databases, you can autogenerate only a single unique value per table.
In many databases, you cannot use the primary key generation mechanism to generate values unique across multiple tables.
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.
public 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:
All code that needs to create new numbers for the same sequence (like
personID
) will share the same sequencer object.Because of the synchronized block, two attempts to get a previously unreferenced sequence at the same instant will not cause two different sequencers to be generated.
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.
private 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:
Check to see if the seed is valid. The seed is invalid if this is a newly generated sequencer or if the seed is exhausted. A seed is exhausted if the next sequence has a value greater than
MAX_KEYS
.If the seed is not valid, get a new seed from the database.
Increment the sequence.
Multiply the seed by
MAX_KEYS
and add that value to the incremented sequence. This is the unique key.
Example 4-9 contains the algorithm.
public 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:
Fetch the current values for the sequence in question from the database.
If the sequence does not yet exist in the database, create it.
Increment the seed from the database.
Update the database
Set the new seed and reset the
sequence
attribute to -1 (this makes the first number generated 0).
You can find the full code for the Sequencer
class
on O’Reilly’s FTP site in the
directory for this
book.
[4] You do not need to be writing a business application to have business logic. Business logic is a generic term that refers to any of the basic concepts in your problem domain. If you are building a first-person shooter game, your “business objects” are monsters, weapons, hazards, and the like.
[5] The value 1,000,000 depends on the system. You will want lower numbers for systems with short uptimes and larger numbers for systems with long uptimes.
Get Java Database Best Practices now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.