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) && v1.name == p1 && 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:
-
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.
-
Any JDOQL parameters become your method signature for a method that must be named "evaluate" and return a boolean.
-
Any JDOQL variables become fields of the class; these must also be declared "public".
-
Any JDOQL imports become Java import statements.
-
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()).
-
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()).
-
In the class that uses the query, import org.xorm.query.CodeQuery so you can use the LANGUAGE constant.
-
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.
|