This document is intended to give a quick overview on how to use XORM. It is not intended to help XORM developers learn the internals of XORM.
The document jumps right to the chase by kicking things off with several examples of using XORM before getting into the details.
More information about XORM can be found on the main page, including how to checkout the source code.
The examples below are based on the following Class Diagram.
This structure is a typical representation of a music library. An Artist or set of Artists record Songs, which may appear on multiple Albums as different Tracks.
Each of these classes is represented by a corresponding table in the database. In addition, the many-to-many relationship between Song and Artist is represented by a many-to-many join table in the database. See the model section below for information on how it maps to DB tables.
This example shows how to read an object from the database using XORM and then traverse its relationships. An explanation follows.
// Package definitions and imports not shown
1 PersistenceManagerFactory factory =
2 XORM.newPersistenceManagerFactory("jdo.properties");
3 PersistenceManager mgr =
4 factory.getPersistenceManager();
5 Query query = mgr.newQuery(Track.class,
6 "song.title == v1");
7 query.declareParameters("String v1");
8 Collection tracks = (Collection)
9 query.execute("A Day in the Life");
10 Iterator trackIter = tracks.iterator();
11 while (trackIter.hasNext()) {
12 Track track = (Track) trackIter.next();
13 System.out.println("Track: " +
14 track.getSong().getTitle() +
15 " appears on " +
16 track.getAlbum().getTitle() +
17 " as number " + track.getNumber());
18 }
The first step in using XORM is to get a PersistenceManager. Most of
the calls you make into XORM are done using the PersistenceManager.
Lines 1-2 involve getting a PersistenceManagerFactory. As input, it
takes the name of a JDO property file that specifies what database and
JDO implementation to use.
Lines 3-4 acquires a PersistenceManager from the factory. Generally,
you create one factory for your application and get
PersistenceManagers from that factory.
Lines 5-6 use the newly acquired PersistenceManager to create a new
query. The query language is specified in the JDO specification.
It's asking for instances of Track, and restricting the Track returned
to Tracks that are recordings of a Song, where the Song has some
title. The query language refers to fields by their property name,
hence Song is specified using a starting lower case letter, "song".
XORM will throw a cryptic exception if you accidentally specify a
property name using the wrong case which is easy to do because the
property name usually matches the class name except for the case of
the first letter.
Line 7 tells XORM that the "v1" we specified in the query is really a
parameter and it's of type String.
Lines 8-9 cause XORM to run the query, using "A Day in the Life" as
the value for "v1". The values for parameters are passed in using the
order declared to XORM in the declareParameters call. The call
returns all the Track objects found in a collection.
Lines 10-11 we go through the returned collection to use the Track.
Note that XORM uses a lazy query mechanism, so even though we called
executeQuery, XORM still hasn't actually run the query against the DB.
Using the returned collection in the while loop causes XORM to finally
run the query.
Line 12 extracts a Track object from the returned collection.
Lines 13-14 we use the returned Track object. First we call getSong()
on the Track. If you look at the data model, every Track belongs to a
Song so the Track interface has a getSong() method on it. Calling
getSong().getTitle() causes XORM to read the Performance
from the DB, allowing you to seamlessly traverse the object hierarchy.
XORM, being lazy, doesn't actually fetch the Performance until the
getTitle() call.
Lines 15-17. All Tracks also have a reference to an Album, and have a
track number attribute. These lines traverse the Album relationship
(again, XORM will load in the Album object on demand) to access the
appropriate properties.
This example shows how to use XORM to modify a DB object and create a
new DB object. XORM is transaction based; so all modifications of
objects MUST be performed inside of a Transaction. Queries and
traversing objects can be done outside the scope of a Transaction.
// Uses PersistenceManager and track objects from Query Example
1 Transaction t = mgr.currentTransaction();
2 t.begin();
3 track.getSong().setTitle("Day in the Life");
4 Song song = (Song) XORM.newInstance(mgr, Song.class);
5 song.setTitle("Fixing a Hole");
6 song.getArtists().add(track.getSong().getArtist());
7 mgr.makePersistent(song);
8 t.commit();
Line 1 gets a reference to a transaction from a PersistenceManager.
Since each PersistenceManager has only one transaction associated with
it, this implies that you cannot use the same PersistenceManager in
different threads at the same time if you are going to be modifying
objects, as they would both be using the same transaction.
Line 2 begins the transaction. XORM does not actually write out
any changes made until the commit on Line 8, but any changes you make
from objects associated with the PersistenceManager that owns the
Transaction will be associated with the new transaction and be written
out to the DB when the Transaction is committed.
Line 3 changes the title of the song for the track.
Line 4 creates a new Song object. XORM tries to be JDO compliant, but
JDO does not specify how to create objects, assuming you will create
them using the normal new method. Because XORM works
with interfaces (see next section), it provides a factory method for
creating objects. The method is static on the XORM class.
Line 5 sets the title of the new song.
Line 6 gets the collection of Artists associated with the new song and
adds a reference to the same artist as the original track. XORM
automatically takes care of dealing with the DB details such as
whether it's a one-to-many or a many-to-many with a
relationship-mapping table. Note also that even though we did not
initialize the collection of Artists to a new instance of
java.util.Collection (there's actually no setArtists() method on the
Song interface), XORM will return an empty collection the first time
getArtists() is called.
Line 7 is a special call telling the PersistenceManager that you want
the passed in object to be stored. If you do not call this method,
the object is considered a transient object that will be garbage
collected normally and not stored to the backing datastore.
Line 8 commits all the changes we've made so far to the DB. It
updates the first Song with the new title and inserts a new Song
object with a join table reference to the performing Artist and the
title set on Line 5.
XORM is an object to relational mapping layer that provides
interface-based persistence. All objects are created by XORM and are
referenced in applications using only interfaces or abstract classes.
This means there is a strict separation of the data model and logic of
the application, while providing an object oriented abstraction of the
data allowing developers to simply use the objects without worrying
about the actual data store.
XORM tries to be JDO (Java Data Objects) compliant where possible but
is not a fully JDO compliant implementation.
When writing an application that uses XORM (see examples above) you
will need to import the JDO interfaces. The most common JDO
interfaces to import are shown below:
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.PersistenceManager;
import javax.jdo.JDOHelper;
import javax.jdo.Transaction;
import javax.jdo.Query;
Those imports provide access to most of the base functionality,
including everything shown in the examples.
JDO generally assumes that objects are created normally using the
standard new construct. However, XORM differs from this
in that it manages all the objects for you, so the application only
deals with the interfaces.
To support this, XORM provides a public class that is used to
bootstrap XORM and is used to provide the non-JDO functionality that
is required. To get the XORM class, you need to use the following
import:
import org.xorm.XORM;
From the transaction example above you can see we called the
newInstance method on XORM when we wanted to create an
object.
The XORM class also provides bootstrap methods named
newPersistenceManagerFactory that are used to get a JDO
PersistenceManagerFactory. After getting the factory, you can use
standard JDO get the PersistenceManager and make queries, use
transactions, etc. For maximum portability, however, you should
instead use the javax.jdo.JDOHelper method
getPersistenceManagerFactory() to acquire a factory. This method is
supported in XORM but is slightly more verbose.
The XORM class also provides some methods you will use less often,
such as getting an object that represents a persistent class's primary
key. You can use that with the PersistenceManager's getObjectById
call to retrieve a specific object if you just know its primary key.
Normally you use a query or an object traversal to get to a specific
object rather than doing a lookup by key.
The XORM class also provides access to the class and DB mapping
(called ModelMapping) which lets you perform some advanced queries
that aren't possible using the JDO query language (or XORM's
implementation of the JDO query language). This usage should be
avoided where possible and is not discussed here; see the user's guide for details.
The first step in using XORM is to create a PersistenceManagerFactory.
When you create a factory you need to pass it the configuration values
to use. Every PersistenceManager created by that factory will use the
same configuration values.
XORM configuration values are specified using the standard Java
properties file.
XORM provides several means of passing in the properties file,
reflected in the many newPersistenceManagerFactory methods it
provides. See the JavaDocs for detailed info. But
you can create factories using the default property file
xorm.properties, by specifying the name of a property file, or by
passing in an input stream that contains a property file. XORM will
try and read in the passed in file and if it can't find it, it will
try to find it in the classpath.
For example, creating a factory using the properties in the file
jdo.properties:
PersistenceManagerFactory factory =
XORM.newPersistenceManagerFactory("jdo.properties");
To run XORM you need to provide several different files including a
property file, a model file, and a database file. Each of these files
is discussed in their own section below. In addition, for each class
defined in the model file, you must provide an interface for the class
with appropriate get/set methods. These interface files are discussed
at the end of the XORM configuration section.
The XORM configuration file that is passed to the
newPersistenceManagerFactory method must have the
following fields:
Property Name |
Example Value |
javax.jdo.PersistenceManagerFactoryClass
|
org.xorm.InterfaceManagerFactory |
The class the JDO service provider makes available that implements the factory interface
|
javax.jdo.option.ConnectionURL
|
jdbc:postgresql://localhost/xorm
|
The JDBC connection URL
|
javax.jdo.option.ConnectionUserName
|
Administrator
|
Name of an authorized DB user
|
javax.jdo.option.ConnectionPassword
|
blah
|
Password of user specified above
|
javax.jdo.option.ConnectionDriverName
|
org.postgresql.Driver
|
The JDBC Driver Manager
|
javax.jdo.option.MinPool
|
2 |
Minimum number of DB connections to have open
|
javax.jdo.option.MaxPool
|
5
|
Maximum number of DB Connections to have open
|
org.xorm.datastore.database
|
/com/mycompany/model-db.xml
|
The resource path to the XML file describing the tables and columns in your database
|
As you can see, the parameters are JDO defined parameters that describe the underlying database to use.
Configuration File Example:
# These are the properties specified by the JDO 1.0 specification
javax.jdo.PersistenceManagerFactoryClass=org.xorm.InterfaceManagerFactory
javax.jdo.option.ConnectionURL=jdbc:postgresql://localhost/xorm
javax.jdo.option.ConnectionUserName=Administrator
javax.jdo.option.ConnectionPassword=
javax.jdo.option.ConnectionDriverName=org.postgresql.Driver
javax.jdo.option.MinPool=2
javax.jdo.option.MaxPool=5
# This property is required by XORM
org.xorm.datastore.database=/com/mycompany/model-db.xml
XORM provides database support for a variety of databases. It
determines which internal driver to use based on the value you provide
for the ConnectionDriverName. XORM currently supports the following
databases:
ConnectionDriverName
Database Supported
org.postgresql.Driver
PostgreSQL
oracle.jdbc.driver.OracleDriver
Oracle
com.microsoft.jdbc.sqlserver.SQLServerDriver
Microsoft SQL Server Engine
com.sybase.jdbc2.jdbc.SybDriver
Sybase
org.hsqldb.jdbcDriver
HSQLDB
com.mysql.jdbc.Driver
MySQL
The XORM database file describes all the tables and columns that XORM
needs to be aware of in the database. Any column not included here
cannot be referenced from XORM or the application through XORM and
will not be set on inserts.
The file is an XML file; the XORM distribution contains a simple DTD
for it. The format is very simple:
<database>
<table name="tablename1">
<column name="column1"/>
<column name="column2" primary-key="true" sequence="s_column2"/>
<column name="column3"/>
</table>
<table name="tablename2">
<column name="column1"/>
<column name="column2" primary-key="true" auto="true"/>
<column name="column3"/>
</table>
<!-- and so on -->
</database>
The file simply lists all the tables and columns. If a table has a
primary key, you can add the primary-key attribute and set it to true.
In addition, you need to define what kind of primary-key it is (XORM
assumes they are always numeric). It supports two kinds of keys: keys
that are derived by querying a sequence (see the sequence attribute on
column2 in tablename1 above), or keys that are automatically added by
the database (see the auto attribute on column2 in tablename2 above).
Generally, all your keys will have either sequence or autoincrement
for every primary key in the file, but which to use depends on the
underlying database. Also refer to the User Guide
and the org.xorm.datastore.option.sequenceNamePattern configuration
parameter to see how to use a global name pattern for sequence names.
The file can be named whatever you wish; it is referenced in the Model Mapping file.
Here is an example using the class diagram at the top of the document as an example:
<?xml version="1.0" encoding="UTF-8"?>
<database>
<table name="ARTIST">
<column primary-key="true" sequence="S_ARTIST_ID" name="ID" />
<column name="NAME" />
</table>
<table name="SONG">
<column primary-key="true" sequence="S_SONG_ID" name="ID" />
<column name="TITLE" />
</table>
<table name="ARTIST_SONG">
<column name="ARTIST_ID" />
<column name="SONG_ID" />
</table>
<table name="ALBUM">
<column name="TITLE" />
<column name="ID" primary-key="true" sequence="S_ALBUM_ID" />
</table>
<table name="TRACK">
<column name="NUMBER" />
<column name="ID" primary-key="true" sequence="S_TRACK_ID" />
<column name="SONG_ID" />
<column name="ALBUM_ID" />
</table>
</database>
This database schema has four entity tables and one many-to-many
linking table. By convention, we have used the "_ID" suffix to
designate foreign key columns. Note that XORM doesn't require you to
specify which columns are foreign keys in the database schema file.
Like the DB file, this file is also an XML file. It uses the JDO
syntax, with some XORM extensions.
It's easiest to illustrate with an example.
<?xml version="1.0" encoding="UTF-8"?>
<jdo>
<package name="com.mycompany.model">
<class name="Artist">
<extension vendor-name="XORM" key="table" value="ARTIST" />
<field name="songs">
<collection element-type="Song">
<extension vendor-name="XORM" key="table" value="ARTIST_SONG" />
<extension vendor-name="XORM" key="source" value="ARTIST_ID" />
<extension vendor-name="XORM" key="target" value="SONG_ID" />
</collection>
</field>
<field name="name">
<extension vendor-name="XORM" key="column" value="NAME" />
</field>
</class>
<class name="Album">
<extension vendor-name="XORM" key="table" value="ALBUM" />
<field name="tracks">
<collection element-type="Track">
<extension vendor-name="XORM" key="table" value="TRACK" />
<extension vendor-name="XORM" key="source" value="ALBUM_ID" />
</collection>
</field>
<field name="title">
<extension vendor-name="XORM" key="column" value="TITLE" />
</field>
</class>
<class name="Song">
<extension vendor-name="XORM" key="table" value="SONG" />
<field name="artists">
<collection element-type="Artist">
<extension vendor-name="XORM" key="table" value="ARTIST_SONG" />
<extension vendor-name="XORM" key="source" value="SONG_ID" />
<extension vendor-name="XORM" key="target" value="ARTIST_ID" />
</collection>
</field>
<field name="title">
<extension vendor-name="XORM" key="column" value="TITLE" />
</field>
</class>
<class name="Track">
<extension vendor-name="XORM" key="table" value="TRACK" />
<field name="number">
<extension vendor-name="XORM" key="column" value="NUMBER" />
</field>
<field name="song">
<extension vendor-name="XORM" key="column" value="SONG_ID" />
</field>
<field name="album">
<extension vendor-name="XORM" key="column" value="ALBUM_ID" />
</field>
</class>
</package>
</jdo>
XORM expects to find an interface or abstract class in the package
with the name specified in the package element, for each class defined
in this file. The interface should have the exact same name,
including case, as the name of the class in this file.
The extension elements are used to insert XORM specific information,
usually mapping a field in a class to a column defined in the Database
file described in the section above.
The extension located inside each class element maps a class to a
table defined in the database file. Similarly, the extension in the
field element maps the element to a specified column. The table and
column names must match the definitions in the database XML file
specified in the .properties file.
Every field is bound to a get or/and set method in
the java file.
To be enhaced a get method must not have args and
a set method must be declared to return void
And remember only abstract methods will be enhaced
XORM assumes that fields and methods follow normal Java variable
name capitalization. This normally means converting from the first
character after get/set from upper case to lower case, but in the
(unusual) special case when there is more than one character
and both the first and second characters are upper case, we leave it alone.
Thus the method "getFooBah()" becomes the field "fooBah" and "getX()"
becomes "x", but "getURL()" stays as "URL".
Relationship Mapping
Things get a bit more complicated when describing relationships to
other classes.
Mapping to a Single Instance
If the class we are describing has a relationship to one and only one
instance of another class, the field element appears identical to any
other class field (like title, etc). XORM can tell by the interface
(described below) that the property returns an interface rather than a
core Java type (String, Date, int, boolean, long, BigDecimal, etc).
When it sees that, it automatically maps the foreign key to the class
via the return type.
For example, on the class Track , there is a field, album . In the Track.java interface the property appears like:
Album getAlbum();
void setAlbum(Album album);
In the model mapping file the property appears like:
<field name="album">
<extension vendor-name="XORM" key="column" value="ALBUM_ID" />
</field>
XORM will automatically map the ALBUM_ID to the ALBUM table.
Mapping a One-To-Many Relationship
When describing a One-To-Many relationship in XORM, you need to tell
XORM what the target table is and what the foreign key in the target
table is that maps to the source object.
This is because the interface will be returning a Collection of
objects, so it can't infer the type from the return class like it can
when traversing the simple case of a mapping to a single instance.
Here we have the relationship on the Album class that describes an
Album as having a collection of Tracks.
<field name="tracks">
<collection element-type="Track">
<extension vendor-name="XORM" key="table" value="TRACK" />
<extension vendor-name="XORM" key="source" value="ALBUM_ID" />
</collection>
</field>
Here the XORM extension defines the collection as coming from the
TRACK table, and telling XORM (using the source key) that it could
find the Tracks for this Album via the ALBUM_ID column on the TRACK
table.
Many-To-Many Mapping
Unlike a one-to-many mapping, a many-to-many mapping relies on a
mapping table that contains the keys of both the source and the target
tables. In model mapping file it looks similar to a one-to-many, but
contains an extra XORM extension that describes the target table since
the source and target tables are no longer the same.
Here is an example of a many-to-many mapping for the Song class. This
mapping shows that a Song can have many Artists performing it.
<field name="artists">
<collection element-type="Artist">
<extension vendor-name="XORM" key="table" value="ARTIST_SONG" />
<extension vendor-name="XORM" key="source" value="SONG_ID" />
<extension vendor-name="XORM" key="target" value="ARTIST_ID" />
</collection>
</field>
The "table" extension tells XORM what mapping table to use, while the
"source" extension lets XORM know what key to use to find the rows
used by source (in this case the Song class). The new extension is
the "target", which says how to map from the mapping table to the
target table, in this case the Artist class.
In XORM, all relationships are one way. In other words, if you need
to traverse a relationship in both directions, you will need to define
the relationship separately in both classes that are involved.
The last set of files that have to be in place to use XORM are the
interfaces. XORM does not generate these from the XML files, they
must be written by hand.
Each field (or property) defined for a class in the model mapping file
must be declared in a corresponding interface.
You may have noticed that none of the files we have looked at so far
have declared any type information, other than collections for many
relationships. XORM uses the return type of the property to determine
the type.
Here is an example of a XORM interface for the Song class:
package com.mycompany.model;
import java.util.Collection;
/**
* A musical work.
*/
public interface Song {
String getTitle();
void setTitle(String title);
Collection getArtists();
}
Properties have a get and set defined for them returning and accepting
the appropriate type. Properties can be read-only or write-only
simply by leaving off the matching set or get as appropriate.
Properties that represent other objects have their get method return
the appropriate interface, like the get/setAlbum in the example above.
Properties that represent "many" relationships have only a get defined
for them that returns a Collection. You can use the collection's
add() or remove() methods to add and remove
objects, and you can use the iterator() method to iterate
through the instances in a collection.
If there are common properties that many interfaces will share, you
can define them in their own interface and have the XORM class
interface extend them. For example, you could create an interface
called Titled and have anything with get/setTitle() extend it.
The interface must be located in the package that is defined by the
name attribute on the package element in the model mapping file. See
the model mapping file description above for details.
The following types are considered built-in persistable types (you
don't need to define an interface or declare them in the model
mapping file, you can just use them).
Type |
Wrapper (if any) |
boolean
|
java.lang.Boolean
|
byte
|
java.lang.Byte
|
char
|
java.lang.Character
|
double
|
java.lang.Double
|
float
|
java.lang.Float
|
int
|
java.lang.Integer
|
long
|
java.lang.Long
|
java.lang.String
|
|
java.math.BigDecimal
|
|
java.util.Date
|
|
Note that the interface can define the wrapper or the base type, XORM
doesn't care.
You can also define abstract classes for your data model objects.
XORM will interpret any abstract methods as properties it should treat
as persistent. Using this approach, you can provide helper methods or
change the visibility of the datastore values to protected or
package-protected. Here's the same example created as an abstract
class with a helper method:
package com.mycompany.model;
import java.util.Collection;
/**
* A musical work.
*/
public abstract class Song {
public abstract String getTitle();
public abstract void setTitle(String title);
public abstract Collection getArtists();
// Helper method
public Artist getFirstArtist() {
if (getArtists().isEmpty()) return null;
return (Artist) getArtists().iterator().next();
}
}
Helper methods are useful for adding custom formatting and
implementing object-oriented business logic that can be accomplished
with a knowledge of the object's relationships.
With your XORM config file, database file, JDO metadata file, and
interfaces for each class described in the model mapping file, you are
ready to actually use XORM.
- XORM has a feature that if the first thing you do with a XORM
returned collection is call size(), it will perform a COUNT on the
objects rather than actually fetching them. This can give a big
performance boost if all you care about is the size of the
collection.
-
If you have a many-to-many mapping table, you should avoid declaring
any columns on the table in your database file, except for the foreign
keys to the tables being mapped. Since mapping tables do not have
unique keys, XORM attempts to update and delete rows by issuing a
where clause with all known values specified. Tables with primary
keys do not have this problem since XORM just uses the primary key.
-
For example, do not put a date modified and/or date created on a
mapping table unless you have DB triggers or other means to ensure
those values are never null.
|