jRelationalFramework

advanced topics

copyright © 2000 is.com

 

These are more in-depth topics that are not covered in the other pages:

Performance

Here are a few things you can do to affect the performance of this framework and limit the number of JDBC calls:
  1. Make sure you are using a Type-2 or Type-4 JDBC driver.  Type-2 drivers talk directly to database specific C libraries.  Type-4 drivers are all Java and do not need any additional software installed on the client and are much faster than the ODBC-bridge (Type-1) JDBC drivers which are not database specific.
  2. When saving objects and performance is important:
    1. If you know you have valid objects use  AbstractDomain#setValidateBeforeSaving(false); This will skip the JDBC calls to validate column UNIQUEness.
    2. If the object being saved will not be used again, use AbstractDomain#setReturnSavedObject(false); to turn off the JDBC calls to find and return the saved object.
  3. Consider using AbstractStaticDomain instead of AbstractDomain for relatively small tables that rarely or never change.  The objects are cached in memory to avoid extra SQL calls.  For example, a Gender table with one row for Male and one row for Female can easily be cached and save the JDBC calls.  (See GenreDomain.java and VideoDomain#postFind()).
  4. Consider aggregating multiple tables into one object.  examples/RentalDomain.java is a good example of using joins to save JDBC calls.
  5. On a similar vein, if you have a one-to-one or many-to-one (not one-to-many) relationship with an object, consider joining the columns for that object, then creating that aggregate object in the postFind() method. See Aggregate Objects.
  6. If performance seems poor when reading many instances with many attributes, override the convertToPersistentObject(aJDBCHelper) method using hard-coded setters.  Do not do this unless you notice a marked difference in performance since it will make the domain class fragile and more complex.

JDBCHelper

This class is used in this framework to make accessing JDBC a little simpler and to facilitate reuse.  This class uses instance variables to store a Connection, Statement, and ResultSet which allows easy sharing between framework methods.  Here is an example of how one can be created:

    JDBCHelper jdbcHelper =
        new JDBCHelper(
            "oracle.jdbc.driver.OracleDriver",
            "jdbc:oracle:thin:@pluto.is.com:1521:testdb",
            "testuser",
            "testpassword");

While it should be rarely necessary to "roll your own" queries like the one below (since the framework does all this for you), here is an example of how the JDBCHelper could be used separate from the framework.  Using the JDBCHelper makes for somewhat cleaner, simpler code. When the JDBCHelper is closed, the default behavior is for the connection for committed and closed.

   List result = new ArrayList(20);
   JDBCHelper helper = null;
   try
       {
       helper = new JDBCHelper("weblogic.jdbc.pool.Driver",
                               "jdbc:weblogic:pool",
                               "tmsPool");
       helper.executeQuery("SELECT * FROM Status");
       while (helper.next())
           {
           StatusValue aStatusValue = new StatusValue();
           aStatusValue.setStatusId(helper.getInteger("StatusId"));
           aStatusValue.setCode(helper.getString("Code"));
           aStatusValue.setActive(helper.getboolean("Active"));
           result.addElement(aStatusValue);
           } // while
       helper.close();
       }
   catch (Exception e)
       {
       try
           {
           helper.rollback();
           helper.close();
           }
       catch (java.sql.SQLException e)
           {
           // The original exception should take precedence
           }
       System.out.println("***" + e);
       e.printStackTrace();
       throw e;
       }
   // use or return the result list here.
 

JDBCHelperPool

This is a new class in version 1.5 that gives access pools of JDBCHelper instances (and hence database connections). Your pool must be initialized before like this before being used:

JDBCHelperPool.createPool(
    "XYZ_DB", // Name of the pool
    JDBCHelperFactory.create(), // or however you create a JDBCHelper
    5); // number of instances in the pool.

If you have more than one database, you can create a separate pool for each database. When a JDBCHelper instance is needed, do something like this:

JDBCHelperPool.getPool("XYZ_DB").getJDBCHelper();

or this:

JDBCHelperPool.getFrom("XYZ_DB");

The above two statements are functionally identical. When you close the JDBCHelper (i.e. aJDBCHelper.close() ) it is automatically returned to the pool and you must retrieve a new one to do any more database calls. However, manually getting and closing JDBCHelper instances is not necessary if you are using the framework to do finds, saves, and deletes.

When you are done with the pool, you can close the connections like this:
JDBCHelperPool.getPool("XYZ_DB").destroy();

It is not a bad idea to use the JDBCHelper pool even if you have a single-threaded application since it will reduce the number of JDBCHelper instances that are created. It will also reduce the overhead of creating so many connections. This pool will keep you from creating more JDBCHelper instances than necessary.

How to use it:
In the setup() method of your AbstractDomain subclass add a line like this:
this.setJDBCHelperPoolName("XYZ_DB");
Then the framework will access that pool whenever it needs a database connection.

postFind()

Override the postFind(aPersistentObject, aJDBCHelper) method in your AbstractDomain subclass whenever you want to do post processing on found objects.  Reasons for doing this might include:
  1. Calculating a value that is not stored in the database.
  2. Setting up relationships/pointers between objects -- i.e. create a complex object.
The JDBCHelper method argument can be used to retrieve column values (i.e. aJDBCHelper.getInteger("MediaId");), but should not be used for additional queries or updates since it is still being used by the framework for the original query. However, a clone of the JDBCHelper can be used for additional queries if needed (i.e. JDBCHelper helper2 = (JDBCHelper) aJDBCHelper.clone();). See VideoDomain#postFind() for a more complete example of retrieving column values.

After postFind() is called, the framework automatically sets the persistent state of the object to CurrentPersistentState.

NO_POST_FIND - Be aware that an infinite loop will occur when using postFind() methods in two domains that end up calling each other. To keep that from happening, one of the postFind() methods should create the other domain with the NO_POST_FIND option. See the CustomerDomain.postFind() example for how this is done.

If you use the save() method with manual transaction control then you need to understand the *Warning* area of the Transactions section.  This may not seem related, but it is because the default behavior of save() is to call find() (which calls postFind()) before returning.  If you are not manually controlling transactions then this is not as much of an issue.
 

find()

AbstractDomain#find(anObject,aJDBHelper) finds one object and should rarely need to be overridden.  The first parameter is either a primary key (Integer, String, etc) or a PersistentObject that has it's primary key(s) populated.  If no object is found with that primary key, null is returned.

Here is the order of what happens inside of a find():

  1. The "where" clause is generated using the primary key column spec.
  2. The "where" clause is appended to the SQL generated from all of the ColumnSpec instances.
  3. The query is executed.
  4. For each row in the result set (There should be only one)
  5. The result is returned.

save()

Click here for an example.  This AbstractDomain method inserts, updates and does nothing, depending on the persistent state of the object being saved.  By default, each call to save() is a complete transaction so you generally don't have to worry about committing or rolling back a transaction.  If an error occurs inside the framework during a save the transaction is automatically rolled back.  See the section on Transactions if you wish to expand a transaction to encompass several database updates.

The last thing the save() method does is to "find" the object it just saved and return it.  The intention of this is to make sure the returned object accurately represents the data in the database.  For example, setting a Timestamp column to NOW in the SQL requires a find for that row to figure out what the database set it to.  In addition, if triggers are used, it is important to pick up the changes that were made to the table by those triggers.

Here is the order of what happens inside of a save.  If this save() call is inside of another transaction, the transaction items below won't do anything.

  1. If the persistent state of the object is current, the object is returned.
  2. The transaction is begun.
  3. preValidate() is called.
  4. validate() is called.
  5. preSave() is called.
  6. The object is either inserted or updated depending upon the persistent state.
  7. postSave() is called.
  8. The transaction is ended
  9. A new object is recreated from the database (via find()) and returned.

preValidate(), preSave(), postSave(), preDelete(), and postDelete()

These methods are called by the framework.  They should be overridden whenever custom behavior is desired before or after a save or delete.  Click here for an example of a preSave() method.

All of these methods have a PersistentObject instance and a JDBCHelper instance as parameters.  The JDBCHelper parameter is the instance that is controlling the transaction.  If you wish to make a JDBC call that is part of that same transaction, then it is important to pass that JDBCHelper along to any other domain calls.

Note that preSave() is called after the validation occurs so you can depend on the object being valid at this point.  See the save() section for the order of events during a save().
 

validate()

The validate(aPersistentObject,aJDBCHelper) AbstractDomain method is called by the save(aPersistentObject,aJDBCHelper) method after preValidate() is called.  Validation can be turned off by sending setValidateOnSave(false) to the AbstractDomain subclass.

The two types of validation done are:

  1. column uniqueness and
  2. required column checking.
Assuming validation is turned on, primary keys are automatically validated for uniqueness before an insert.  Non-primary key columns should specify UNIQUE in the ColumnSpec constructor in order to be validated for uniqueness.  When a uniqueness check fails, DuplicateRowException is thrown.

If a column is listed as REQUIRED and it returns null (or an empty string), then MissingAttributeException is thrown.
 

Table joining

There are two ways to join tables.
  1. Specify a JoinTable instance with JoinColumns in the setup() method of the AbstractDomain subclass.
  2. Specify a custom join when you call findWhere() or findWhereOrderBy().

Current Timestamp

Putting the value of JRFConstants.CURRENT_TIMESTAMP into a timestamp attribute will signal the framework to use the database-specific function for the current date and time in any INSERT or UPDATE statement it generates.  For example, in Oracle, 'SYSDATE' is used for the database-specific function.  In SQLServer and Sybase, 'GetDate()' is used.
 

Optimistic Locking

Optimistic locking of objects has several advantages to pessimistic locking.  First, no one will ever be locked out of an object because someone pulled up an object on their screen and then went to lunch (or on vacation!).  Second, users who read and update objects relatively quickly are very unlikely to run into conflicts with other users.  Users who read an object, go to a meeting, and then do their updates are more likely to have conflicts, but at least they aren't keeping other people from doing their work!  Third, web environments are much more conducive to optimistic locking.

Using a Timestamp or an Integer column as an optimistic lock is very easy with this framework.  A Timestamp column that is defined as an optimistic lock is automatically updated to the current time whenever the object is saved.  An Integer optimistic lock column is automatically incremented by one when the object is saved.  If the object is updated by another user between the time the current user reads the object and saves it back to the database, an ObjectHasChangedException will be thrown to the current user.  The other users object changes can be retrieved with anObjectHasChangedException.

Click here for an example of how to set one up.

Note that, because some database's Timestamps (like Oracle's) are only precise to the nearest second, it is possible that two users could update the same object in the same second.  In that case, the data from the first save would be lost without any warning or error message.  For these databases it is better to use an Integer column for the optimistic lock.

DEFAULT_TO_NOW should always be used for the Timestamp default value.  When updates occur, the value of the timestamp lock is included in the WHERE clause so that if someone else has changed that row since it was read, the update will not occur and ObjectHasChangedException will be thrown.  As mentioned before, If the update does occur, the table timestamp is updated with the current date and time.
 

Aggregate Objects

An aggregate object in this framework is one that is built from the result set for another object. For example, if you have a Customer row with columns for the address and you want those columns to be in their own Address object, the Address object would be an aggregate object.

To create an aggregate object, use a null for the ColumnSpec setters (optional) and override the postFind() method to access those columns directly to create the aggregate object. This area could be a little more automated in the future, but at least this can be done right now.

For an example, see the Media object creation code in VideoDomain#postFind()
 

Exception Handling

There are two types of exceptions thrown by this framework.  Checked exceptions, which can be expected during normal usage, and unchecked exceptions.
  1. Checked exceptions are subclasses of DomainException and are only thrown during save operations when the validateBeforeSaving flag is on.  These are database-independent.
  2. Unchecked exceptions are subclasses of RuntimeException and are as follows:

Transactions

You won't need to think much about transactions if you want the transaction to span only one call to save() (which also calls preSave(), postSave()) or delete(). Each of these will begin and end (or rollback if an error occurs) their own transactions.

This framework uses the database's transaction mechanism to group updates into one transaction.  For multiple updates to share a transaction, the same java.sql.Connection (and hence the same JDBCHelper) instance must be used. See ExampleTest.test000Setup() in the examples directory of the distribution zip file for an example.

If you want to enlarge the scope of the transaction to more than one domain update call, do something like this...

public void saveThings(List things)
    throws
    ObjectHasChangedException,
    MissingAttributeException,
    DuplicateRowException
  {
  JDBCHelper jdbcHelper = this.getJDBCHelper();
  this.beginTransaction(jdbcHelper);
  Iterator iterator = things.iterator();
  while (iterator.hasNext())
    {
    Thing aThing = (Thing) iterator.next();
    new OtherThingDomain().save(aThing.getOtherThing(), jdbcHelper);
    this.save(aThing, jdbcHelper);
    this.executeSQLUpdate("DELETE FROM temptable", jdbcHelper);
    }
  this.endTransaction(jdbcHelper);
  }

Notice how we used the same JDBCHelper object for beginning the transaction, doing the saves, doing the custom SQL, and ending the transaction. Assuming that only domain methods are used to do the updates, the framework will catch any exception, rollback the transaction, and rethrow the exception so there is NO NEED to put the call to endTransaction(jdbcHelper) into a finally block.

Or similarly if the code is outside of a Domain instance:
  ....
  JDBCHelper jdbcHelper = JDBCHelperPool.getFrom("XYZ_DB");
  jdbcHelper.beginTransaction();
  i_personDomain.save(aPerson, jdbcHelper);
  i_partDomain.save(aPart, jdbcHelper);
  jdbcHelper.endTransaction();
  jdbcHelper.close(); // return it to the pool
  ....

Notice that the above code does NOT NEED to explicitly catch exceptions in order to rollback because all updates are done inside of domain methods. Any exception that occurs inside of a domain update automatically rolls back the transaction.

Transaction Gotchas
Some of the below stuff gets pretty complex. If you want to read it, go ahead, but otherwise ignore it and keep it in mind for when you run into transaction problems. Your answer may be here.

*Warning*  If you are manually beginning and ending a transaction using Oracle or another multi-threaded database it is important that you use the same JDBCHelper instance for any queries inside of that transaction that wish to access any new or changed table rows. (That was a mouthful :-).  A different JDBCHelper instance will not see the changes made until the original transaction is committed.

This issue will most likely show up if you are using postFind() to attach an object that was saved inside the same transaction, because (as of version 1.2 and 1.3) postFind() forces you to clone the JDBCHelper if you wish to use it to find other objects.  Remember that the default behavior of a save() is to do a find() at the very end before returning (see save() above).  If you hope to use the object returned from a save() you need to be aware of this potential.

There is at least one solution to keep people from getting tripped-up on this, but it involves a lot of code changes.  Hopefully, in a future version this problem can be addressed.  We are working very hard to make this framework as intuitive to use as possible.

XADataSource and Transactions - If you are using an XA DataSource you will need to make sure that JDBCHelper has shouldCommitOnClose set to false. Also, If you need to do multiple updates you can safely do it by beginning the transaction at the top of the method and close the JDBCHelper at the bottom without ever ending the transaction. XADataSources don't allow you to manually commit(end) a transaction, but you must close the JDBCHelper in order to return the connection to the pool.
 

Subtype Tables

(As of version 1.7) It is often desirable to use subtype tables for storing instances of a class hierarchy. For example, a Person class could have an Employee subclass. It is convenient then to put the additional employee fields into a separate EMPLOYEE table that has the same primary key as the PERSON table.

jRF, like most frameworks, makes a few assumptions about your implementation in order to keep the complexity-level of the code lower:

  1. The subtype table must have it's primary key column name and type match the supertype table exactly. This is good design anyways since it clearly identifies the subtype tables relationship with its supertype table.
  2. The class hierarchy can currently only go one level deep. This limitation may be removed at some point.
  3. If there is a row in the subtype table, there must be a row in the supertype table.
An example can be found in the EmployeeDomain class in the test directory of the distribution. Here are the steps to code the proper jRF classes for a subtype/supertype table arrangement. We'll use the Person/Employee example
  1. Create an Person subclass of PersistentObject.
  2. Create a Employee subclass of Person with any additional fields.
  3. Create a PersonDomain class as normal. Only create column specs for the Person fields (those that will go into the PERSON table).
  4. Create an EmployeeDomain subclass of PersonDomain. Set the subtype table name: e.g. setSubtypeTableName("EMPLOYEE"). Add the non-primary key column specs with the addSubtypeColumnSpec() method. The primary key column spec(s) should NOT be added to the subtype domain class since it is assumed the primary key matches the super class.
Performance considerations:
 

ResultPageIterator

This class (in package com.is.jrf) is used for paging forwards and backwards through a result set.  This capability is most often needed for web applications that only want to show a given number of results at a time.  In JSP, the ResultPageIterator instance would need to be stored in the session or passed between requests because it keeps track of what page it is on.  The following example pages through all customers with 10 customers per page.

CustomerDomain customerDomain = new CustomerDomain();
ResultPageIterator iterator =
     new ResultPageIterator(customerDomain, 10)
       {
       List doFind(AbstractDomain domain)
          {
          return domain.findAll();
          }
       };

 while (iterator.hasNext())
    {
    List results = iterator.nextPage();
    // do something with this page of 10 objects...
    }
 

top
main page
noticed a document error?
copyright © 2000 is.com