To learn how to obtain a Broker instance lets have a look at the
Application class. The public constructor of this class looks like
follows:
 |  |  |
 |
public Application()
{
PersistenceBroker broker = null;
try
{
broker = PersistenceBrokerFactory.createPersistenceBroker("repository.xml");
}
catch (Throwable t)
{
t.printStackTrace();
}
useCases = new Vector();
useCases.add(new UCListAllProducts(broker));
useCases.add(new UCEnterNewProduct(broker));
useCases.add(new UCDeleteProduct(broker));
useCases.add(new UCQuitApplication(broker));
}
|  |
 |  |  |
We just ask the PersistenceBrokerFactory to create an instance that
uses the file ./repository.xml as mapping repository (more details on
this repository in the section on the Object
/Relational mapping). This broker instance is reached to the
constructors of the UseCases. The constructors just store it in a
protected attribute broker for
further usage.
Retrieving collections and iterators
The next thing we need to know is how to use this broker instance to help us
in our persistence operations. In this use case we have to retrieve a
collection containing all product entries from the persistent store. To
retrieve a collection containing objects matching some criteria we can use
PersistenceBroker::getCollectionByQuery(Query query).
Where Query is a Class that allows to formulate criteria like
price > 100. In our case we want to select all persistent
instances, so we need no filtering criteria, they can thus be left
null.
Here is the code of the UCListAllProducts::apply()method:
 |  |  |
 |
public void apply()
{
System.out.println("The list of available products:");
// build a query that selects all objects of Class Product, without any
// further criteria according to ODMG the Collection containing all
// instances of a persistent class is called "Extent"
Query query = new QueryByCriteria(Product.class, null);
try
{
// ask the broker to retrieve the Extent collection
Collection allProducts = broker.getCollectionByQuery(query);
// now iterate over the result to print each product
java.util.Iterator iter = allProducts.iterator();
while (iter.hasNext())
{
System.out.println(iter.next());
}
}
catch (Throwable t)
{
t.printStackTrace();
}
}
|  |
 |  |  |
If you don't need the resulting collection for further reference as in this
example where we just want to iterate over all products once, it may be a
good idea not to use getCollectionByQuery() but getIteratorByQuery() which
returns an Iterator.
This method is extremely useful if you write applications that have to
iterate over large resultsets. Instances are not created all at once but
only on demand, and instances that are not longer referenced by the
application my be reclaimed by the garbage collector. Using this method the
code will look like follows:
 |  |  |
 |
public void apply()
{
System.out.println("The list of available products:");
// build a query that select all objects of Class Product, without any
// further criteria according to ODMG the Collection containing all
// instances of a persistent class is called "Extent"
Query query = new QueryByCriteria(Product.class, null);
try
{
// ask the broker to retrieve an Iterator
java.util.Iterator iter = broker.getIteratorB
// now iterate over the result to print each product
while (iter.hasNext())
{
System.out.println(iter.next());
}
}
catch (Throwable t)
{
t.printStackTrace();
}
}
|  |
 |  |  |
For further information you may have a look at the
PersistenceBroker JavaDoc
and at the Query JavaDoc.
Storing objects
Now we'll have a look at the use case UCEnterNewProduct. It works
as follows: first create a new object, then ask the user for the new
product's data (productname, price and available stock). These data
is stored in the new objects attributes. Then we must store the newly
created object in the persistent store. We can use the method
PersistenceBroker::store(Object obj)
for this task.
 |  |  |
 |
public void apply()
{
// this will be our new object
Product newProduct = new Product();
// now read in all relevant information and fill the new object:
System.out.println("please enter a new product");
String in = readLineWithMessage("enter name:");
newProduct.setName(in);
in = readLineWithMessage("enter price:");
newProduct.setPrice(Double.parseDouble(in));
in = readLineWithMessage("enter available stock:");
newProduct.setStock(Integer.parseInt(in));
// now perform persistence operations
try
{
// 1. open transaction
broker.beginTransaction();
// 2. make the new object persistent
broker.store(newProduct);
broker.commitTransaction();
}
catch (PersistenceBrokerException ex)
{
// if something went wrong: rollback
broker.abortTransaction();
System.out.println(ex.getMessage());
ex.printStackTrace();
}
}
|  |
 |  |  |
Maybe you have noticed that there has not been any assignment to
newProduct._id, the primary key attribute. On storing of newProduct
OJB detects that the attribute is not properly set and assigns a
unique id. This automatic assignment of unique Ids for the Attribute
_id has been eplicitly declared in the XML-Repository (see section
'Defining the Object/Relational Mapping' below).
Updating Objects
Editing and updating a product entry works quite similar. The user
enters the products unique id and the broker tries to lookup the
respective object. This lookup is necessary as our application does
not hold a list of all products. The found product is then edited and
then stored. The PersistenceBroker uses the .store(...)
method for inserting new objects as well as for updating existing
objects. Here is the code:
 |  |  |
 |
public void apply()
{
String in = readLineWithMessage("Edit Product with id:");
int id = Integer.parseInt(in);
// We do not have a reference to the selected Product.
// So first we have to lookup the object,
// we do this by a query by example (QBE):
// 1. build an example object with matching primary key values:
Product example = new Product();
example.setId(id);
// 2. build a QueryByExample from this sample instance:
Query query = new QueryByExample(example);
try
{
// 3. start broker transaction
broker.beginTransaction();
// 4. lookup the product specified by the QBE
Product toBeEdited = (Product) broker.getObjectByQuery(query);
// 5. edit the existing entry
System.out.println("please edit the product entry");
in = readLineWithMessage("enter name (was " + toBeEdited.getName() + "):");
toBeEdited.setName(in);
in = readLineWithMessage("enter price (was " + toBeEdited.getPrice() + "):");
toBeEdited.setPrice(Double.parseDouble(in));
in = readLineWithMessage("enter available stock (was " + toBeEdited.getStock()+ "):");
toBeEdited.setStock(Integer.parseInt(in));
// 6. now ask broker to store the edited object
broker.store(toBeEdited);
// 7. commit transaction
broker.commitTransaction();
}
catch (Throwable t)
{
// rollback in case of errors
broker.abortTransaction();
t.printStackTrace();
}
}
|  |
 |  |  |
Deleting Objects
The UseCase UCDeleteProduct allows the user to select one of the
existing products and to delete it from the persistent storage. The
user enters the products unique id and the broker tries to lookup the
respective object. This lookup is necessary as our application does
not hold a list of all products. The found object must then be
deleted by the broker. Here is the code:
 |  |  |
 |
public void apply()
{
String in = readLineWithMessage("Delete Product with id:");
int id = Integer.parseInt(in);
// We do not have a reference to the selected Product.
// So first we have to lookup the object,
// we do this by a query by example (QBE):
// 1. build an example object with matching primary key values:
Product example = new Product();
example.setId(id);
// 2. build a QueryByExample from this sample instance:
Query query = new QueryByExample(example);
try
{
// start broker transaction
broker.beginTransaction();
// lookup the product specified by the QBE
Product toBeDeleted = (Product) broker.getObjectByQuery(query);
// now ask broker to delete the object
broker.delete(toBeDeleted);
// commit transaction
broker.commitTransaction();
}
catch (Throwable t)
{
// rollback in case of errors
broker.abortTransaction();
t.printStackTrace();
}
}
|  |
 |  |  |
I did use a QueryByExample in this case as it needs a minimum of code
for primary key lookups. But you can also build a query based on
filter criteria. In this use case the query building would look like
follows:
 |  |  |
 |
// build filter criteria:
Criteria criteria = new Criteria();
criteria.addEqualTo(_id, new Integer(id));
// build a query for the class Product with these filter criteria:
Query query = new QueryByCriteria(Product.class, criteria);
...
|  |
 |  |  |
Finishing this application is left as an exercise to the interested reader
Now you are familiar with the basic functionalities of the OJB
PersistenceBroker. To learn more you might consider implementing the
following additional use cases:
-
List all products with a price > 1000 (or let the user enter a
criteria)
- Delete all products that have a stock of 0
-
increase the price of all products that cost less then 500 by 11%
(and make the changes persistent)
Defining the Object/Relational Mapping
After looking at the code of the tutorial application and at the
sample database (build browse_db will
start a browser on the InstantDB database) you will probably ask:
How is it possible for the OJB Broker to store objects of class
Product in the table PRODUCT
without any traces in the sourcecode? or How does OJB know that the
database column NAME is mapped onto
the attribute name?.
The answer is: It's done in the OJB Metadata Repository. This
Repository consists of a set of classes describing the O/R mapping
(have a look at the package ojb.broker.metadata).
The repository consists of ordinary Java objects that can be created
and modified at runtime. This brings a lot flexibility for special
situations where it is neccessary to change the mapping
dynamically.Keeping the mapping dynamic has several advantages:
- No preprocessing of Java sourcecode, no fix compiled persistence classes
-
Mapping can be inspected and changed at runtime, allows maximum flexibility
in changing persistence behaviour or in building your own persistence layers
on top of OJB.
But it has also at least one disadvantage: Performance. Due to the
dynamic approach OJB uses the slow Java Reflection API to inspect and
modify business objects. But we took great care in reducing the
reflection overhead to a minimum.
In the following sections I will show how the O/R mapping is
defined for the tutorial application.
The persistent class Product
There is only one persistent class in our tutorial application,
the class Product. Here it's definition:
 |  |  |
 |
package org.apache.ojb.tutorial1;
/**
* represents product objects in the tutorial system
*/
public class Product
{
/** product name*/
protected String name;
/** price per item*/
protected double price;
/** stock of currently available items*/
protected int stock;
...
}
|  |
 |  |  |
I ommited the method definitions, as they are not relevant for the
O/R mapping process.
The corresponding database table
Now we take a look at a corresponding table definition, in SQL DDL
(I give the instantDB syntax here, the syntax may vary slightly for
your favourite RDBMS):
 |  |  |
 |
CREATE TABLE PRODUCT (
ID INT PRIMARY KEY,
NAME CHAR(100),
PRICE DOUBLE,
STOCK INT
)
|  |
 |  |  |
You will notice that I added a primary key column ID.
This is an artificial attribute as it not derived from
the domain model. To use such an artificial key instead of a compound
key of domain attributes is recommended but not mandatory for O/R
mappings. If you are using such artificial keys you have to modify
the original class layout slightly and include a corresponding
attribute. OJB requires that all primary key columns of a
table are reflected in attributes in the coresponding class. This is
one of the very few intrusions of the persistence layer
into your business code, because these attributes are not necessarily
part of the OOD model.
So we must change our initial class definition slightly to match
this requirement:
 |  |  |
 |
public class Product
{
/** this is the primary key attribute needed by OJB to identify instances*/
private int _id;
/** product name*/
protected String name;
/** price per item*/
protected double price;
/** stock of currently available items*/
protected int stock;
}
|  |
 |  |  |
Apart from the primary key attribute there is no further instrusion
of persistence code into our business object. No need to extend a
base class or to implement any interfaces. That's why we claim that
OJB provides transparent persistence.
There is one important exception: persistent capable classes must
provide a public default constructor. Implementing a constructor that
initializes all persistent attributes is recommended for performance
reasons but not required. OJB will print out warnings when such a
constructor is missing. When you run the tutorial application you
will see such a warning as the class Product
does not provide an initializing constructor.
The Mapping
Now we have to describe the mapping from the class Product
to the database table PRODUCT. This
is typically not done programmatically but by declaration in a
repository xml file. The DescriptorRepository
class provides factory methods to boot itself from this XML file. The
resulting repository can be manipulated programmaticaly later. (It's
also possible to build up a complete repository programmatically.)
We have to write our own mapping and integrate it into the OJB
sample repository in src/test/ojb/repository.xml.
This XML file looks like follows:
 |  |  |
 |
<?xml version="1.0" encoding="UTF-8"?>
<!-- This is a sample metadata repository for the ObJectBridge System.
Use this file as a template for building your own mappings-->
<!-- defining entities for include-files -->
<!DOCTYPE descriptor-repository SYSTEM "repository.dtd" [
<!ENTITY user SYSTEM "repository_user.xml">
<!ENTITY junit SYSTEM "repository_junit.xml">
<!ENTITY internal SYSTEM "repository_internal.xml">
]>
<descriptor-repository version="0.9.1" isolation-level="read-uncommitted">
<!-- The Default JDBC Connection. If a class-descriptor does not specify its own JDBC Connection,
the Connection specified here will be used. -->
<!--jdbc-connection-descriptor
platform="@DBMS_NAME@"
jdbc-level="@JDBC_LEVEL@"
driver="@DRIVER_NAME@"
protocol="@URL_PROTOCOL@"
subprotocol="@URL_SUBPROTOCOL@"
dbalias="@URL_DBALIAS@"
username="@USER_NAME@"
password="@USER_PASSWD@"
/-->
<jdbc-connection-descriptor
platform="Hsqldb"
jdbc-level="2.0"
driver="org.hsqldb.jdbcDriver"
protocol="jdbc"
subprotocol="hsqldb"
dbalias="/samples/hsql/OJB"
username="sa"
password=""
/>
<!-- include user defined mappings here -->
&user;
<!-- include mappings for JUnit tests and sample apps here -->
&junit;
<!-- include ojb internal mappings here -->
&internal;
</descriptor-repository>
|  |
 |  |  |
This file contains a lot of information:
-
the XML file is validated against the DTD
repository.dtd.
This enforces syntactical correctness
of the xml file. Be sure to keep the dtd file alway in the same
directory as the xml file, otherwise the XML parser will complain
that it could not find the DTD.
-
The mapping contains already a default
JDBCConnectionDescriptor. Such a
Descriptor contains information about JDBC connections used for
persistence operations. OJB allows to have one
JDBCConnectionDescriptor per class.
But it is also possible to define a default Descriptor. The JDBC
connection defined by this descriptor is used for all classes that do
not have a specific JDBCConnectionDescriptor.
In our example the Descriptor specifies that by default all
operations have to use the HsqlDb JDBC driver and that the
database is located in samples directory has to be used.
-
Mappings for OJB regression tests and sample applications
-
The OJB internal mappings. OJB needs some
internal tables, e.g for maintaining locks, auto counters and the
ODMG collections and Maps. The corresponding mappings are contained
here. They are essential for the proper operation of the system and
may not be modified