In a past article on XML Databinding, I showed how you could work with Java objects and have them persist as XML files.
In this article, I'll discuss Sun's Java Data Objects (JDO) standard. JDO allows you to persist Java objects, supporting transactions and multiple users. It differs from JDBC in that you don't have to think about SQL and "all that database stuff." It differs from serialization as it allows multiple users and transactions. This standard allows Java developers to use their object model as a data model, too. There is no need to spend time going between the "data" side and the "object" side.
Various products -- including CocoBase, WebGain TOPLink, and Castor JDO -- try to do this for you. Now that there is a standard way to do this, we get the benefit of only having to learn one way to do it. This is just like when JDBC came along and allowed us to work with any database that provided a driver. Great stuff!
We will take the Address Book example from my XML data binding article and "port" it over the JDO world. The JDO implementation that I will use is OpenFusion JDO from Prism Technologies. As you'll see, there is only one part of the code that uses a PrismTech API; everything else uses the standard JDO API (with the implementation hidden behind the scenes).
The code for this article can be downloaded as a zip file here. |
We will walk through the following tasks:
Person
object that we wish to persist
in the database.
PersonPersist
object that will take care of
persisting, reading, and updating the Person
s in the datastore.
We'll start with the same Person
object we defined in the XML
article. This object follows the standard JavaBean conventions of
get
and set
on the attributes. Notice that although we
are persisting this class, there is nothing special about it. It doesn't have to
inherit or implement any persistent interface/base class. The requirements for a
class that can be persisted are:
public
, or
set*
methods).
Thread
, File
, Socket
). With these requirements in mind, let's look at Person.java
:
public class Person {
private String name;
private String address;
private String ssn;
private String email;
private String homePhone;
private String workPhone;
// -- allows us to create a Person via the constructor
public Person(String name, String address, String ssn,
String email, String homePhone, String workPhone) {
this.name = name;
this.address = address;
this.ssn = ssn;
this.email = email;
this.homePhone = homePhone;
this.workPhone = workPhone;
}
// -- accessors
public String getName() { return name; }
public String getAddress() { return address; }
public String getSsn() { return ssn; }
public String getEmail() { return email; }
public String getHomePhone() { return homePhone; }
public String getWorkPhone() { return workPhone; }
// -- mutators
public void setName(String name) { this.name = name; }
public void setAddress(String address) {
this.address = address;
}
public void setSsn(String ssn) { this.ssn = ssn; }
public void setEmail(String email) { this.email = email; }
public void setHomePhone(String homePhone) {
this.homePhone = homePhone;
}
public void setWorkPhone(String workPhone) {
this.workPhone = workPhone;
}
}
So we have a Person
object that we want to work with. Now we
need to create something that will manage the persistence. Let's walk through
the code, and see how we:
main()
method. Here we have the beginning of the PersonPersist
object. We
import the standard JDO classes and the ManagedConnectionFactory
from the OpenFusion implementation. We could have abstracted that out to a
separate class, of course. The constructor sets the connection factory, using
the javax.jdo.PersistenceManagerFactoryClass
property. This is like
setting the database driver property in the JDBC world.
package addressbook;
import java.util.*;
import javax.jdo.*;
import
com.prismt.j2ee.connector.jdbc.ManagedConnectionFactoryImpl;
public class PersonPersist
{
private final static int SIZE = 3;
private PersistenceManagerFactory pmf = null;
private PersistenceManager pm = null;
private Transaction transaction = null;
// Array of people to persist
private Person[] people;
// Vector of current object identifiers
private Vector id = new Vector(SIZE);
public PersonPersist() {
try {
Properties props = new Properties();
props.setProperty("javax.jdo.PersistenceManagerFactoryClass",
"com.prismt.j2ee.jdo.PersistenceManagerFactoryImpl");
pmf = JDOHelper.getPersistenceManagerFactory(props);
pmf.setConnectionFactory( createConnectionFactory() );
} catch(Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
The connection factory is created in a static method named
createConnectionFactory()
. This factory needs the JDBC URL, the
JDBC driver, a username, and a password. OpenFusion JDO also comes with a
DBHelper
that looks for a properties file containing the database
settings to use.
public static Object createConnectionFactory() {
ManagedConnectionFactoryImpl mcfi = new
ManagedConnectionFactoryImpl();
Object connectionFactory = null;
try {
mcfi.setUserName("scott");
mcfi.setPassword("tiger");
mcfi.setConnectionURL(
"jdbc:oracle:thin:@localhost:1521:thedb");
mcfi.setDBDriver("oracle.jdbc.driver.OracleDriver");
connectionFactory = mcfi.createConnectionFactory();
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
return connectionFactory;
}
Related Reading
|
Here we have some real meat. The persistPeople()
method creates
three people, using the constructor that we saw in the Person.java
file. Then we see JDO at work. The first thing we do is get a persistence
manager via getPersistenceManager()
. Then we create a transaction
where we will do our work (and we commit that work after we do it). To persist
this object graph we simply call the makePersistentAll( Object[] )
method. The for()
loop at the bottom of the code gets the unique ID
for the persistent objects, and saves them away for later use.
public void persistPeople() {
// create an array of Person's
people = new Person[SIZE];
// create three people
people[0] = new Person("Gary Segal", "123 Foobar Lane",
"123-123-1234", "gary@segal.com",
"(608) 294-0192", "(608) 029-4059");
people[1] = new Person("Michael Owen",
"222 Bazza Lane, Liverpool, MN",
"111-222-3333", "michael@owen.com",
"(720) 111-2222", "(303) 222-3333");
people[2] = new Person("Roy Keane",
"222 Trafford Ave, Manchester, MN",
"234-235-3830", "roy@keane.com",
"(720) 940-9049", "(303) 309-7599)");
// persist the array of people
pm = pmf.getPersistenceManager();
transaction = pm.currentTransaction();
pm.makePersistentAll(people);
transaction.commit();
// retrieve the object ids for the persisted objects
for(int i = 0; i < people.length; i++) {
id.add(pm.getObjectId(people[i]));
}
// close current persistence manager to ensure that
// objects are read from the db not the persistence
// manager's memory cache.
pm.close();
}
Here are some of the other methods that you can call on the persistence manager. The three categories are:
Make instances persistent | Delete persistent instances | Make instances transient |
makePersistent(Object o) |
deletePersistent(Object o) |
makeTransient(Object o) |
makePersistentAll(Object[] os) |
deletePersistentAll(Object[] os) |
makeTransientAll(Object[] os) |
makePersistentAll(Collection os) |
deletePersistentAll(Collection os) |
makeTransientAll(Collection os) |
Our display code starts by getting the persistence manager (as all the code
will do). We use the object IDs that we saved in the
persistPeople()
method above to give us our object back. Once we
have our object, we can call the methods that the object implements -- in this
case get
s to give us our data back. At this point you are probably
seeing there isn't a lot of code needed to persist your objects.
public void display(int end) {
Person person;
int max = end <= SIZE ? end : SIZE;
// get a new persistence manager
pm = pmf.getPersistenceManager();
// retrieve objects from datastore and display
for(int i = 0; i < max; i++) {
person = (Person) pm.getObjectById(id.elementAt(i),
false);
System.out.println("Name : " + person.getName());
System.out.println("Address : " +
person.getAddress());
System.out.println("SSN : " + person.getSsn());
System.out.println("Email : " + person.getEmail());
System.out.println("Home Phone: " +
person.getHomePhone());
System.out.println("Work Phone: " +
person.getWorkPhone());
}
pm.close();
}
The code to change a Person
that exists in the datastore is
simple, too. It should look very similar to the code to display the "people."
Here we are creating a transaction (since we are modifying the row), changing
the name using the setName()
method that we defined, and finally,
committing the transaction to save the changes back. The only real difference
between this operation and working with transient objects is that we are
thinking about transactions.
public void change() {
Person person;
// retrieve objects from datastore
pm = pmf.getPersistenceManager();
transaction = pm.currentTransaction();
// change DataString field of the second persisted object
person = (Person) pm.getObjectById(id.elementAt(1),
false);
person.setName("Steve Gerrard");
// commit the change and close the persistence manager
transaction.commit();
pm.close();
}
Could you have guessed the code needed to delete the second person from the
datastore? You know all of the information. Looking at the code below you will
see that we are using the deletePersistent()
method mentioned in
the persistence manager methods in Step Two.
public void delete() {
// retrieve objects from datastore
pm = pmf.getPersistenceManager();
transaction = pm.currentTransaction();
// delete the 2nd persisted object from the datastore and
// its id from Vector id.
pm.deletePersistent(pm.getObjectById(id.remove(1),
false));
// commit the change and close the persistence manager
transaction.commit();
pm.close();
}
Finally, this program has a main()
to run through, persist the
people, change one, and then delete it. If you run this, you will see the
address book displayed at each point.
public static void main(String[] args) {
System.out.println("Create PersonPersist");
PersonPersist personPersist = new PersonPersist();
System.out.println("Setup and persist a group of people");
personPersist.persistPeople();
System.out.println("Display the persisted people");
personPersist.display(SIZE);
System.out.println("Change a name ");
personPersist.change();
personPersist.display(SIZE);
System.out.println("Delete a person ");
personPersist.delete();
personPersist.display(SIZE - 1);
}
We now have all the code for our application. The next step that we need is
to create a JDO descriptor that the JDOEnhancer will use. "What is the
JDOEnhancer?", I hear you scream. The JDO architecture is built with the idea
that a JDO implementation can take the bytecode for your classes and manipulate
them to add needed functionality. For example, the JDOEnhancer will make the
class implement the PersistanceCapable
interface (so you don't have
to), and may implement some of the methods in that interface. So we will see
that after we compile our code, we will have to run the JDOEnhancer to do the
bytecode manipulation (this is something that Thought Inc. doesn't like about
JDO). We need to create a descriptor file that gives information about the
classes that we wish to persist. The file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jdo SYSTEM
"file:/D:/Apps/OpenFusionJDO/xml/schema/jdo.dtd">
<jdo>
<package name="addressbook">
<class name="Person" identity-type="datastore">
</class>
</package>
</jdo>
This is a basic file, but works for our needs. There is more complicated mapping available, and you can check the spec at Section 18: XML Metadata. Here is a slightly more complex mapping from the OpenFusion examples:
<class name="Department" identity-type="datastore">
<field name="name"/>
<field name="employees">
<collection element-
type="com.prismt.j2ee.jdo.examples.appKeyDepartment.Employee">
</collection>
</field>
<field name="manager"/>
</class>
</package>
</jdo>
Now that we have the code and the JDO descriptor, let's put it all together and see how to build the system.
There are only a couple of steps to build the system that we have created:
We all know how to run javac
right? We just have to make sure
that we set up the CLASSPATH
correctly before we run it. Here is an
example running Windows:
% set OPENFUSION_DIR=D:\Apps\OpenFusionJDO
% set
CLASSPATH=%OPENFUSION_DIR%\lib\connector.jar;%OPENFUSION_DIR%\
lib\jndi.jar;%OPENFUSION_DIR%\lib\log4j.jar;%OPENFUSION_DIR%\l
ib\xerces.jar;%OPENFUSION_DIR%\lib\classes12.zip;%OPENFUSION_D
IR%\lib\jdo.jar;%OPENFUSION_DIR%\lib\jta-
spec1_0_1.jar;%OPENFUSION_DIR%\lib\ofjdo.jar;.
% javac –d . Person*.java
The JDOEnhancer takes the compiled code that we just created and the JDO descriptor file that we built previously. Here is the full syntax of the OpenFusion JDOEnhancer:
java com.prismt.j2ee.jdo.enhancer.JDOEnhancer
Mandatory Options:
-cp base directory to begin searching for classes to be
enhanced. This is not the CLASSPATH, just where our
compiled persistent classes are
-oc directory to place the enhanced classes
-pd JDO descriptor file(s)
Optional:
-db specific target database [oracle, sybase, etc]
-od directory to generate SQL scripts to
Here is an example of running the JDOEnhancer for our application:
% java com.prismt.j2ee.jdo.enhancer.JDOEnhancer -oc . -pd
person.jdo -db oracle -od db -cp .
The JDOEnhancer can create database scripts to set up the database for us, as
long as we give it the –db
and –od
switches. It will
create lots of separate scripts, but one will be called
load_all.sql
. Open that file and load it into your favorite SQL
prompt (e.g., sqlplus
for Oracle).
Take a peek at the Oracle version that is created for our application:
CREATE SEQUENCE instid_seq INCREMENT BY 1
;
CREATE TABLE JDO_addressbook_Person_SCO
(
inst_id INTEGER NOT NULL,
class INTEGER NOT NULL,
JDO_address VARCHAR2(255),
JDO_email VARCHAR2(255),
JDO_homePhone VARCHAR2(255),
JDO_name VARCHAR2(255),
JDO_ssn VARCHAR2(255),
JDO_workPhone VARCHAR2(255)
)
;
CREATE TABLE JDO_addressbook_Person
(
inst_id INTEGER NOT NULL,
class INTEGER NOT NULL,
JDO_address VARCHAR2(255),
JDO_email VARCHAR2(255),
JDO_homePhone VARCHAR2(255),
JDO_name VARCHAR2(255),
JDO_ssn VARCHAR2(255),
JDO_workPhone VARCHAR2(255)
)
;
CREATE TABLE prismjdoProp
(
name VARCHAR2(255) PRIMARY KEY,
value VARCHAR2(255)
)
;
CREATE TABLE prismjdoExtents
(
class_id NUMBER(38,0) PRIMARY KEY,
class_name VARCHAR2(255) UNIQUE,
app_key VARCHAR2(255)
)
;
ALTER TABLE JDO_addressbook_Person_SCO ADD PRIMARY KEY
(inst_id, class)
;
ALTER TABLE JDO_addressbook_Person ADD PRIMARY KEY (inst_id,
class)
;
INSERT INTO prismjdoExtents VALUES(0, 'addressbook.Person',
'com.prismt.j2ee.jdo.spi.DBKey')
;
COMMIT WORK
;
INSERT INTO prismjdoProp VALUES('USE.RDBMS.TRIGGERS', 'true')
;
COMMIT WORK
;
Now the database is set up; we can run the application and see it roll!
% java addressbook.PersonPersist
Grab the code and try it out!
We have shown how to work with the new JDO standard, using the OpenFusion JDO implementation. This is a new world, where developers can focus on their business needs and work with objects without having to be SQL gurus. I hope JDO takes off, even though some people in the industry think that the spec isn't quite there yet.
Dion Almaer is a Principal Technologist for The Middleware Company, and Chief Architect of TheServerSide.Com J2EE Community.
Return to ONJava.com.
Copyright © 2004 O'Reilly Media, Inc.