User's Guide

Introduction

System Requirements

XORM requires a compliant Java 1.4 virtual machine. 1.4 was chosen for the availability of a standard logging API and other useful features. Other versions of the Java runtime are not supported. XORM has no platform-specific code. Please note that using XORM with a relational database will typically require a JDBC driver which may engender its own system requirements.

Installation

To install XORM, extract the files from the distribution. The distribution comes with the complete internal JavaDoc for XORM as well as this HTML-based documentation in the "docs" subdirectory. The complete source is in the "src" directory; other subdirectories contains unit tests and some examples to help get you started.

To use XORM, you will need the JAR files from the "lib" subdirectory present in your classpath for compilation and execution (with the exception of "junit.jar", which is needed only for the unit tests). In addition to these files supplied with the distribution, you will need to acquire the following, which are not redistributable by XORM:

Both of these are available from Javasoft. The best way to acquire the JDO API JAR is to download the JDO specification (PDF and JAR); the JTA JAR can be downloaded separately (follow the link for "Class Files").

At runtime, you'll also need to supply a JDBC driver compatible with XORM. Currently, XORM supports PostgreSQL, MySQL, HSQLDB, Sybase, Microsoft SQL Server Engine and Oracle relational databases. Other SQL-92 compliant databases should be trivial to support as long as JDBC drivers exist.

JDO Compatibility

While XORM's design philosophy differentiates it from the implementation suggested by the Java Data Objects specification, XORM aims to be as compatible with the JDO spec as possible, and all JDO functionality is written to be as portable as possible. This section delineates XORM's compatibility with the JDO specification.

The following notes are for informational purposes only and are subject to change as development continues. Because XORM does not require a reference enhancer, it is difficult to test it against the official Test Compatibility Kit (TCK) for JDO from the specification committee. When available we will post specific results from this testsuite.

In general, XORM supports the basic JDO functionality required to deploy an application. Here are some specific JDO features that are currently not implemented.

  • No support for second class objects (SCOs), including collection types as SCOs. (Note that this does not mean there is no support for collection relationships, only that you cannot persist java.lang.* types as the contents of collections.)
  • Unsupported optional features: nontransactionalWrite, optimistic transactions.
  • JDOQL support is not fully compliant; some complex expressions are not parsed correctly.
  • J2EE integration: XORM does not support a managed transactional environment at this time.
  • PersistenceManager cache eviction methods are not yet supported.
  • XORM object instances are not portable to different JDO implementations.
  • XORM does not yet support persistence of class hierarchies.
  • Only datastore identity is supported.

Developing with XORM

Writing Data Model Objects

In XORM, data objects are just that — simple repositories for fielded data that can be persisted in a datastore. Developers define the interfaces for their persistence capable objects using the standard JavaBeans naming conventions. A XORM data object looks exactly like any other Java interface or abstract class. XORM works by enhancing the interface or abstract class at runtime in order to manage all bean-style properties.

Simple properties should be defined with getXXX() and setXXX() methods. These method pairs can return and take as input any persistable Java type as a parameter. Persistable types are one of the following:

  • primitive types (boolean, byte, char, double, float, int, long, short)
  • wrappers for primitive types (Boolean, Byte, Character, Double, Float, Integer, Long, Short)
  • java.lang.String
  • java.math.BigDecimal
  • java.util.Date
  • any persistence capable user type (as defined in a .jdo file)

It is also possible to define read-only (and even write-only) properties by excluding the set() or get() method. However, due to the factory style construction for XORM objects, the only real case you would want to have a read-only property would be to expose an existing column in a datastore.

XORM also supports collections. Collections represent 1-to-N or M-to-N (many to many) relationships between different types in an object model. For collection relationships, only the getXXX() method is needed. XORM will automatically initialize all new collections to an empty collection (a getXXX() method that returns a Collection will never return null).

There are no other requirements for defining interfaces. XORM interfaces and abstract classes do not need to extend or implement any other interfaces or classes, nor be in a particular package. While it is convenient (and recommended) to place related objects in the same Java package, this is not mandatory. XORM follows the JDO conventions for finding persistence metadata (see below).

The methods you define can be package-scope for purposes of data hiding.

Creating New Objects and Object ID References

XORM provides static methods on helper class org.xorm.XORM to acquire new instances of PersistenceCapable interfaces. The XORM specific methods you will need are the following:

  public Object XORM.newInstance(PersistenceManager mgr, Class mappedClass);
  public Object XORM.newObjectId(Class mappedClass, Object id);

The first method is used to construct new instances of XORM objects. It is the equivalent of using the "new" keyword with PersistenceCapable classes. Objects returned from this method are in the transient, non-persistent state.

The newObjectId() method (there are variants for convenient wrapping of int and long types) creates an ObjectId instance that can be passed to the getObjectById() methods on the JDO interfaces. This is provided so that programmers can avoid creating trivial ObjectId classes for XORM's datastore identity persistence.

Configuring XORM

XORM is configured via a set of interrelated files that must be present at runtime. One is a standard JDO properties file that configures global options and datastore connection information. This file can be placed anywhere that it can be read from the Java process. Next, a XORM-specific file describes the layout of relational data. Finally, a JDO XML metadata file includes XORM extension elements to map Java classes and fields to datastore tables and columns.

Configuring Properties

The properties file should contain at least the following properties, matching the standard naming conventions set by the JDO specification.

javax.jdo.PersistenceManagerFactoryClass=org.xorm.InterfaceManagerFactory
javax.jdo.option.ConnectionURL=JDBC database URL
javax.jdo.option.ConnectionDriverName=JDBC driver class
javax.jdo.option.ConnectionUsername=database login
javax.jdo.option.ConnectionPassword=database password
javax.jdo.option.MinPool=minimum number of pooled connections
javax.jdo.option.MaxPool=maximum number of pooled connections

You must also specify the following property for XORM:

org.xorm.datastore.database=/path/to/database.xml

This property is required and should reference the classpath-based location of your database schema XML (see below).

Optional Properties

XORM has several additional properties that are not required but can be used to further fine-tune the runtime system. In the descriptions below, bold text indicates the default value for the parameter, if it is omitted from the properties file.

org.xorm.option.ValidateXML=true|false

By default, XORM will validate the XML in the JDO and database definition files against their respective DTDs. JDK 1.4 comes with a validating parser, Crimson. However, if you are using a non-validating parser that is configured to bypass Crimson, you should turn validation off.

org.xorm.option.ThreadLocalTransactions=true|false

Enabling this option means that instead of having a one-to-one mapping with a JDO Transaction, a given PersistenceManager will have one Transaction per active thread. This can be useful in a multithreaded environment such as a web server to allow concurrent access to objects. Each transaction operates in its own context. Note that using this approach is not compliant with the JDO specification and is not portable to any other JDO implementation.

org.xorm.FetchGroupManagerClass=org.xorm.FetchGroupManager

This option allows you to override the default fetch group management by providing your own implementation. This might be useful to provide custom data retrieval algorithms best suited to your application.

org.xorm.cache.DataCacheClass=org.xorm.cache.SoftReferenceCache

You can override the cache implementation used by XORM for its factory-level cache. If you set this value to "none", no second-level cache will be used. This may be necessary in an environment where other applications are changing the data that would otherwise be held in the cache. For best results with the default cache, you should check the details of how soft references are collected on your platform.

XORM also provides an implementation of a Least Recently Used (LRU) cache. This cache contains hard and soft references, and can be configured with a bias toward either. To use the LRUCache, add the following properties:

org.xorm.cache.DataCacheClass=org.xorm.cache.LRUCache
org.xorm.cache.LRUCache.hardSize=500
org.xorm.cache.LRUCache.softSize=-1
org.xorm.cache.LRUCache.logInterval=-1

The "hardSize" property sets the maximum number of objects that will be kept strongly referenced in the cache and must be a non-negative integer. "softSize" sets the maximum number of SoftReferences that will be kept; a value of -1 indicates that SoftReferences should be used as much as possible (simulating the behavior of SoftReferenceCache); 0 means use no SoftReferences. The "logInterval" setting specifies a minimum delay in millisecs for logging cache utilization information. Setting this to -1 disables logging.

org.xorm.datastore.ConnectionInfoClass=
  org.xorm.datastore.sql.SQLConnectionInfo

This option is generally necessary only if you are providing your own implementation of a datastore driver. It can be set to org.xorm.datastore.xml.XMLConnectionInfo to use the XML datastore driver. With the default setting, the following additional options are available:

org.xorm.datastore.sql.IdleCheck=300000

Specifies a time in milliseconds after which a JDBC connection will be tested if it has not been used. The connection will be tested just before it is given to a datastore driver. A value of -1 means never check and a value of 0 means always check. The default value is 5 minutes. If a connection is bad, it will be removed from the pool (and replaced).

org.xorm.datastore.sql.CheckReturnedConnection=true|false

This option enables checking at the time the connection is returned to the pool as well. It can be expensive in terms of I/O, but can ensure fast detection of database error conditions.

org.xorm.datastore.sql.LastIDStatement=
  default based on javax.jdo.option.ConnectionDriverName
org.xorm.datastore.sql.NextIDStatement=
  default based on javax.jdo.option.ConnectionDriverName

These options allow additional configuration of XORM's default SQL generation engine to work with databases that are otherwise SQL 92 compliant but have their own mechanisms for generating autoincrement or sequence numbers. These strings can contain the token "{0}", which will be replaced at runtime with the sequence name specified in the database XML file. The default settings for these options are kept in the org/xorm/datastore/sql/drivers.properties file.

org.xorm.datastore.sql.TransactionIsolationLevel=
  READ_UNCOMMITTED|READ_COMMITTED|REPEATABLE_READ|SERIALIZABLE

The default setting for this option is datastore-dependent. Overriding this option with one of the values delineated above causes all JDBC connections in the pool to be set to the specified transaction isolation level.

Configuring Database Layout

If you have an existing database, you can reverse-engineer its layout into a XORM database XML file by running ant generate-dbxml -Dproperties=my-jdo.properties -Doutput=my-database.xml (substitute your properties file from the setup above and pick a name for your database XML file). To run this, you'll need to make sure your JDBC driver JAR is in XORM's lib directory. You can also create your database XML by hand using a text editor or XML editor.

The database XML file looks like this:

<database>
  <table name="tableName">
    <column name="columnName"
               [type="dataType"]
               [read-only="true|false"]
	       [non-null="true|false"]
               [primary-key="true|false"] 
               [auto="true|false"] 
               [sequence="sequenceName"] />
    <column ... />
  </table>
  <!-- more tables -->
</database>

The type attribute is optional. If unspecified, XORM will use the driver's default mapping of Java types to datastore types when inserting or updating values. For SQL-based drivers, you can specify dataType as a String matching one of the constant value names from java.sql.Types. Some examples are "varchar", "clob", "numeric" and "decimal".

The read-only attribute is optional. If a column is marked as read-only, you will not be allowed to map it to a property for which a set() method exists. In addition, when inserting or updating the object in the database, the column will never be touched, allowing the column's value to be maintained outside the application (a good example might be timestamp fields that are updated by database triggers or defaults).

The non-null attribute is optional. If non-null is set to "true", it indicates that the column does not support null values. This information is used by XORM in some cases to determine whether ANSI SQL outer joins are necessary.

The primary-key attribute is optional and signifies that the column is a primary key for the table. Currently only single-column primary keys are supported.

The sequence attribute indicates that the named sequence is used to get a value to use for the column. Depending on whether the "auto" attribute is indicated, the sequence value will be read before or after the insert.

The auto attribute indicates that the column is configured in the database to use an autoincrement mechanism, and that the value for the column should be read from the database after insertion. It is typically used on its own for databases that provide an autoincrement or identity type, and in combination with the sequence attribute for databases that do not.

Configuring JDO Metadata

JDO metadata is defined in a file with a ".jdo" extension that must be available in the virtual machine's classpath at runtime. The JDO convention is to place the {package}.jdo file for a package in the classpath at the same level as the package folder itself. So if the JDO file is for the com.mycompany.foo package, you would need the following in your classpath:

/com/
/com/mycompany/
/com/mycompany/foo.jdo  <-- the JDO file is named {package}.jdo
/com/mycompany/foo/MyClass.class

Alternatively, if your JDO XML file contains the mapping for just one class (instead of a full package), it should be named {class}.jdo and sit in the same directory as your class file (in this case, it would be /com/mycompany/foo/MyClass.jdo).

You can generate package-level skeletal JDO files by running the following: ant generate-jdo -Dpackage=com.mycompany.foo -Doutput=filename.jdo -Dclasses=mysrc/classes. As you can see from the parameters, you'll need to point the ant task at the location of your compiled Java classes. The tool will perform introspection on each class it finds in the specified package, and if it is abstract or an interface, a JDO XML section will be generated. You'll then need to edit the JDO file and fill in the blanks. You can, of course, do all this manually as well.

XORM supports most of the standard JDO XML tags and attributes as defined in the JDO DTD. There are currently some limitations:

  • Only datastore identity (the default) is allowed as an option for attribute "identity-type" of tag "class"; as a consequence, attribute "objectid-class" of classes is not applicable, nor is attribute "primary-key" of fields.
  • XORM currently does not support non-flattened mappings of class hierarchies, so specifying "persistence-capable-superclass" or "requires-extent" has no effect. (Note that in flattening the hierarchy, XORM allows you to map fields of superclasses or superinterfaces, which is contrary to the JDO spec, but can sometimes be more useful.)
  • The field attribute "persistence-modifier" is not used by XORM; instead, all properties left as abstract or interface methods will be considered persistent. There is currently no provision for transactional, but non-persistent, properties.
  • Collections of heterogeneous types are not currently supported. You must specify a value for the "element-type" attribute of a "collection" tag.
  • All embedding related tags ("embedded-element", "embedded-key", etc.) are ignored by XORM.
  • Maps and arrays are not currently supported.

In addition to the standard JDO tags (package, class, field, etc.), XORM requires extension tags to be used. Each is distinguished by using the XML attribute vendor-name="XORM" along with a particular key and value.

For each class, you must provide the following tags:

In the body of the class tag,

<extension vendor-name="XORM" key="table" name="TABLE_NAME" />

For each field that is not a Collection:

In the body of the field tag,

<extension vendor-name="XORM" key="column" name="COLUMN_NAME" />

For each Collection field:

In the body of the <collection> tag,

<extension vendor-name="XORM" 
  key="table" 
  value="TABLE_NAME" /> <!-- optional, needed for many-to-many -->
<extension vendor-name="XORM" 
  key="source" 
  value="SOURCE_COLUMN" />
<extension vendor-name="XORM" 
  key="target" 
  value="TARGET_COLUMN" /> <!-- optional, needed for many-to-many -->
<extension vendor-name="XORM" 
  key="order-by" 
  value="COLUMN [ASC|DESC]{, COLUMN...}" /> <!-- optional -->

If the table for the collection is the same as the table specified in the mapping for the element-type class, the "table" extension may be omitted (this is usually the case with one-to-many relationships, but not many-to-many relationships.) Refer to the examples subdirectory of the distribution, or the tutorial, for further guidance.

Configuring Logging

You can control the logs emitted by XORM by editing the file {jre_directory}/lib/logging.properties (see the java.util.logging package for details on how the logging mechanism works). XORM creates loggers associated with particular implementation classes. This allows you to easily change the logging level for various aspects of the system.

You can trace the SQL generated by XORM by setting org.xorm.datastore.sql.level to INFO or higher (this is the default setting for all logging in Sun's Java runtime). If you don't want to see the generated SQL, add the following line:

org.xorm.datastore.sql.level=WARNING

Extensions to JDO

Invalidating the Factory-Level Cache

XORM maintains a factory-level cache unless explicitly turned off. This cache is used to minimize database access and increase performance. However, in some cases, it is necessary to clear this cache -- in general this is the case if an outside application makes changes to the data held in the cache. The org.xorm.XORM class has a static method, clearCache(), to handle this situation.

Building Native SQL Queries

XORM internal APIs can be used to execute native SQL queries that return persistence capable objects. This can be useful to accomplish things that may not be directly possible with JDOQL, or may be unsupported in XORM's JDOQL implementation. Direct SQL queries should be used sparingly, as they are decidedly non-portable.

Just like using JDOQL, you use the PersistenceManager query factory API to construct SQL queries. queries, but you pass in a DataQuery variable instead of a JDOQL filter String.

You'll need to import the following two classes:

import org.xorm.query.DataQuery;
import org.xorm.datastore.sql.SQLCondition;

Then use it in code like this:

Class candidateClass;
SQLCondition sqlCondition;
Query query = mgr.newQuery(DataQuery.LANGUAGE, 
                  new DataQuery(candidateClass, sqlCondition);

You construct an org.xorm.datastore.sql.SQLCondition by giving it a collection of Tables to join and a "where clause". The tables listed must include all the tables you reference in your where clause.

You get a valid org.xorm.datastore.Table reference from the ModelMapping instance associated with a PersistenceManagerFactory. There's a static method on the XORM class to get at this (actually, there are two versions: one takes a PersistenceManager, one takes a PersistenceManagerFactory).

Here's an example:

   import org.xorm.ModelMapping;
   import org.xorm.XORM;
   import org.xorm.query.DataQuery;
   import org.xorm.datastore.sql.SQLCondition;
   // etc

   ModelMapping mapping = XORM.getModelMapping(mgrOrFactory);
   ArrayList tables = new ArrayList();
   tables.add(mapping.getTable("BOOK"));
   tables.add(mapping.getTable("AUTHOR"));
   String sql =
       "BOOK.AUTHOR_ID = AUTHOR.ID AND AUTHOR.NAME LIKE {name}";
   Query query = mgr.newQuery(DataQuery.LANGUAGE, 
     new DataQuery(Book.class, new SQLCondition(tables, sql)));
   query.declareParameters("String name");
   // find books by Fitzgerald, Fitzpatrick, etc.
   Collection books = (Collection)
       query.execute("Fitz%"); 

Note that you declare parameters just like you would in JDOQL, and the syntax for referencing them in the where clause is using the curly braces, i.e. "{varName}". However, there is no syntax escaping (the values of your variables will simply be copied into the SQL string), so be careful not to use single quotes in Strings, for example.

Filtered Relationships

With XORM, you can adorn a -to-many relationship with a filter just like any other JDO Query. The collection is defined in the Java source as taking whatever parameters are necessary.

  public Collection getCompatibleWidgets(String p1, String p2);

XORM will implement this method for you when you specify the "filter" key when defining a collection field in the JDO metadata.

<field name="compatibleWidgets">
  <collection element-type="Widget">
    <extension vendor-name="XORM" key="table" value="mfr_widget" />
    <extension vendor-name="XORM" key="source" value="mfr_id" />
    <extension vendor-name="XORM" key="target" value="widget_id" />
    <extension vendor-name="XORM" key="filter" 
     value="protocols.contains(v1) &amp;&amp; v1.name == p1 &amp;&amp; v1.version == p2" />
    <extension vendor-name="XORM" key="parameters" 
     value="String p1, String p2" />
    <extension vendor-name="XORM" key="variables" value="Protocol v1" />
  </collection>
</field>

Note that you must use XML escapes for the ampersand character. Valid extension keys for queries of this sort are "filter", "parameters", "variables" and "ordering". They each correspond to the JDOQL counterpart methods.

One additional "pseudo-parameter" is available for the filter clause on a filtered relationship. The "owner" parameter is defined by XORM to refer to the object which the method is being invoked on. (Note that "this" has a different meaning in the concept of JDOQL).

CodeQuery Mechanism

XORM provides another query mechanism that allows you to write your queries as compiled Java code. At runtime, XORM does the work of instantiating the candidate objects and running them through your logic. There's no need for the "Java within Java" syntax of JDOQL, and there is a direct mapping from JDOQL to compilable Java code. On the other hand, you can do a lot more complex stuff in Java code than you will ever be able to do in JDOQL.

Right now this is implemented in a very resource-intensive manner. All candidates are retrieved, and then all combinations of variables are retrieved and tested against each candidate (in the worst case). Future versions may be more optimized.

The requirements for a code query are the following:

  1. Define an abstract class that extends or implements the persistent object class/interface you are querying against. This is done so that you have a valid "this" reference and can call local methods, etc. Due to Java class format issues (a long-lived bug on the JDC), the abstract class must either be an outer class, or be defined as "public static" if it is an inner class.
  2. Any JDOQL parameters become your method signature for a method that must be named "evaluate" and return a boolean.
  3. Any JDOQL variables become fields of the class; these must also be declared "public".
  4. Any JDOQL imports become Java import statements.
  5. JDOQL fields should be converted to method calls (getXXX()). The semantics of any methods that change persistent object state are undefined. You can't use the JDOQL shortcuts for testing String equality with "==" (use equals()) or Dates with "<" or ">" (use before() and after()).
  6. If the evaluation can throw a NullPointerException, you can either explicitly check for it in your code, or allow it to be thrown, in which case a return value of "false" will be assumed (the same is true for any other exception thrown from evaluate()).
  7. In the class that uses the query, import org.xorm.query.CodeQuery so you can use the LANGUAGE constant.
  8. Ordering can be done by implementing the Comparable interface. That means you can do arbitrarily complex logic for ordering.

The following example comes from the JDO specification. The query finds all departments that include an employee whose salary is greater than the parameter supplied.

JDOQL:

Query q = mgr.newQuery(Department.class, 
  "emps.contains(emp) & emp.salary > sal");
q.declareParameters("float sal");
q.declareVariables("Employee emp");
Collection deps = (Collection) q.execute(new Float(30000.));

The same query can be expressed using the CodeQuery mechanism in XORM as the following (assuming Department is defined as an abstract class):

public abstract class DeptsWithEmpSalary extends Department {
  public Employee emp;  // variable
  public boolean evaluate(float sal) {
    return emps.contains(emp) && emp.getSalary() > sal;
  }
}

To execute the query, the following code snippet is used:

Query q = mgr.newQuery(CodeQuery.LANGUAGE, DeptsWithEmpSalary.class);
Collection deps = (Collection) q.execute(new Float(30000.)); 

Tools and Utilities

Compiling the source

The build.xml script in the XORM installation directory contains several tasks to ease compilation and JDO development. You'll need to install Jakarta Ant to use these. The XORM distribution comes with a precompiled JAR file of the core classes needed at runtime. If you'd like to use any of the additional tools or utilities, you'll need to compile XORM from the included source code with the following tasks:

ant compile

Compiles the core classes, but does not build any JAR files.

ant tools

Compiles the tools directory, not normally needed at runtime and thus left out of the default build.

ant examples

Compiles the XORM examples (not included in the default build).

ant tests

Compiles the XORM JUnit tests (not included in the default build).

ant dist

Creates a xorm.jar file in the "dist" directory. This JAR will include all the classes you have chosen to compile using the above listed tasks.

ant dist-full

Like "dist", but creates a JAR file that includes all the other dependencies from the "lib" directory, as well as the classes from the "tools" directory. The resulting JAR can be double-clicked or executed with "java -jar xorm-full.jar" to invoke the XORM configuration tool described below.

ant clean

Deletes all compiled classes.

ant javadoc

Generates Java API documentation in the "docs/api" subdirectory.

Configuration Tool

XORM comes with a configuration tool that provides a graphical user interface for creating and editing JDO properties. Use the "dist-full" task described above to build this tool into a JAR that can be double-clicked to launch, or use the following ant task:

ant gui

Object Editor

The following interactive, text-based tool can be used to create and edit object data and relationships between objects. It is somewhat limited, however, and mostly useful for testing purposes.

ant object-editor
 -Dproperties=my-jdo.properties

Note that you'll need to make sure your JDBC driver and any other supporting classes (including your persistent objects) are in the system classpath, or you're likely to get classloader errors.

Database Utilities

The build script defines the following tasks for integration with relational databases:

ant generate-dbxml
 -Dproperties=my-jdo.properties -Doutput=my-database.xml
 [-DtablePattern="{regular expression}"]

This task creates a XORM database XML file by examining the actual table configuration of your database. The relevant connection properties must be configured in "my-jdo.properties" before invoking the task. You can optionally specify a regular expression, and only tables with matching names will be included in the output.

ant generate-jdo 
 -Dpackage=com.mycompany.foo
 -Doutput=filename.jdo 
 -Dclasses=mysrc/classes

Generates skeletal JDO files for the specified package. You'll need to point the script at your compiled classes.

ant generate-interfaces
 -Dabstract=true|false -Dpackage=com.whatever.model 
 -Dproperties=../whatever/xorm.properties 
 -Doutput=../whatever/src/com/whatever/model 

This tool is intended to produce skeletal interfaces or abstract classes directly from database metadata. You can specify where the generated code goes on the file system, what the package name is, and whether the tool should output interfaces (the default) or abstract classes. The "output" option defaults to the current directory.

Tests and Examples

XORM's test suite is currently incomplete, but the existing tests can be invoked with ant.

ant run-tests

Runs the JUnit tests defined in the "tests" directory source tree.

ant run-books

Runs the simple book library example application included with XORM. You'll need to configure the books.properties and books-db.xml files for your datastore.

$Header: /cvsroot/xorm/xorm/docs/guide.html,v 1.19 2003/07/20 20:35:02 dcheckoway Exp $