PriDE Introduction
What is
PriDE?
Initialization
of PriDE
Mapping of data
Generation of
entity classes
Standard
operations
Transaction
handling
Error handling
Accessing
multiple databases
Prepared
Statements
Stored Procedures
Further reading
What is PriDE?
PriDE stands for 'Primitive Database Environment'
and is a simple O/R mapper for relational databases. O/R mapping is the
wide-spread
approach to map records of a relation SQL database to objects of an
object-oriented
application. The application should operate on its entities as
object-oriented
as possible, not regarding that some of them come from a database or
must
be saved in one. PriDE provides an infrastructure to
- Describe the mapping of database tables to Java classes
- Read and write data records without programming SQL statements
- Simplify transaction and connection management
- Unify the exception handling
In principle, the database access is performed via JDBC, version 2.0
and uses plain standard SQL for communication. Therefore, PriDE is
basically suitable for any databases supporting SQL 92 syntax. PriDE
was designed for usage in J2SE and J2EE environments and is used
identically in both except some initialization operations. The
framework follows a very pragmatic approach to provide basic
development support quickly and easily. It does not claim to conform
with established persistence management standards but follows
common design patterns and proved to be suitable in mission-critical
projects
over many years now. The feature list PriDE-Features.html
may help to figure out wether PriDE meets the requirements of
individual development projects, and allows to roughly compare this
toolkit with existing well-known O/R mapping products and standards
like EJB CMP 2.0 and JDO. PriDE is probably the smallest O/R mapping
framework available and thus meets the very key requirement for a
persistence manager:
If persistence management is a critical problem,
the persistence manager must be an uncritical solution
PriDE is so small that it can actually be understood in any single
line of its code, providing the developer full control over how data is
exchanged between an SQL database and a Java application. Its
simplicity is based on
a few important (and partly unpopular) design principles which are
explained in PriDE-Inside.html. The
following examples gives a short impression about how PriDE works in an
application:
// Initialization
DatabaseFactory.setResourceAccessor(myAccessor);
DatabaseFactory.setDatabaseName(myDatabase);
// Record creation
Customer customer = new Customer(01, "lessner");
customer.create();
// Single record retrieval
customer = new Customer(01);
customer.find();
// Record update
customer.setName("Lessner");
customer.update();
// Record deletion
customer.delete();
// Record query
ResultIterator iter = customer.query("name");
do {
System.out.println(customer.getId());
} while(iter.next());
// End of transaction
DatabaseFactory.getDatabase().commit();
|
The operations above are explained in the following sections step by
step. Basically, the application just deals with the following classes
from the PriDE library.
The class Database
makes up the heart of the framework although its operations are usually
not
called directly. In general, there are the following different levels
of
abstraction, the user may work on:
- Level 4
The convenience classes MappedObject
and ValueObjectAdapter
provide ready-to-use database operations on an per-object basis. This
is the actual O/R mapping level, being explained in detail below.
- Level 3
The convenience classes above are based on the methods createRecord(),
updateRecord(),
fetchRecord()
and deleteRecord()
of class Database.
They perform database operations using mappings between Java objects
and database records described by class RecordDescriptor.
The exact principle is explained in the following sections. Additional
methods on this level for object queries are query(), queryAll()
und wildcardSearch().
- Level 2
The methods sqlUpdate()
und sqlQuery()
allow low-level database queries and modifications. These methods
should only be used in very special cases. They are at least embedded
in the framework's SQL logging and connection management.
- Level 1
The function getConnection()
provides a connection to the database which allows to run any ordinary
JDBC calls. This method should also only be used in very special cases,
if there are actually no suitable secure and comfortable means of the
framework suitable. At least the function benefits from the connection
management of an underlying ResourceAccessor.
Initializing PriDE
As already mentioned above, the database access is performed through
the central class Database. An instance of this class can be
fetched from the DatabaseFactory
calling the static function getDatabase().
This requires a preceeding initialization of the DatabaseFactory to
have suitable resources available like
- database driver
- database URL
- SQL protocol
The link to physical resources is provided by implementations of the
interface ResourceAccessor
following a strategy pattern for database access. PriDE provides the
classes ResourceAccessorJ2SE,
ResourceAccessorWeb
and ResourceAccessorJ2EE,
making the framework suitable for standard environments as well as
servlet engines or application servers. The following example
demonstrates the initialization of PriDE which has to be performed only
once per application (e.g. in the main() function):
Properties props = new Properties();
props.setProperty(ResourceAccessor.Config.DRIVER,"sun.jdbc.odbc.JdbcOdbcDriver");
props.setProperty(ResourceAccessor.Config.USER, "sa");
props.setProperty(ResourceAccessor.Config.PASSWORD, "");
props.setProperty(ResourceAccessor.Config.LOGFILE, "sql.log");
ResourceAccessorJ2SE accessor = new
ResourceAccessorJ2SE(props);
DatabaseFactory.setResourceAccessor(accessor);
DatabaseFactory.setDatabaseName("jdbc:odbc:mydb");
|
The classes ResourceAccessorWeb and ResourceAccessorJ2EE
assume a database being available as a DataSource
by a JNDI lookup. The initialization is very similar and may look like
this:
Properties props = new Properties();
props.setProperty(ResourceAccessor.Config.USER, "sa");
props.setProperty(ResourceAccessor.Config.PASSWORD, "");
props.setProperty(ResourceAccessor.Config.LOGFILE, "sql.log");
ResourceAccessorJ2EE accessor = new
ResourceAccessorJ2EE(props);
DatabaseFactory.setResourceAccessor(accessor);
DatabaseFactory.setDatabaseName("jdbc.mydb");
|
When using an application server, there is a suitable place to be
found for such a static initialization. The pattern collection includes a general
recommendation for that case. The usage of multiple databases is
explained below. A complete list of all available configuration
properties for the pre-defined resource accessor classes is listed at PriDE-Config.html.
Mapping of data
PriDE basically supports the mapping of records in a database to Java
objects and vice versa. The results of a query are iteratively copied
from
a JDBC ResultSet
in a Java entity object of a corresponding type. For database
manipulation of the database, the data is extracted from an entity
object to dynamically assemble an SQL statement from it. The mapping
details are fetched from a
descriptor and per reflection from the entity object. The class RecordDescriptor
is used to provide mapping descriptions. The following example
illustrates the usage of descriptor classes:
For a database table described by
CREATE TABLE customer
(
id
INTEGER NOT NULL,
name
VARCHAR(20) NOT NULL,
CONSTRAINT PK_customer PRIMARY KEY (id)
);
|
a corresponding Java entity type may be defined like this:
class Customer extends MappedObject {
protected static RecordDescriptor red =
new RecordDescriptor
(Customer.class,
"customer", null, new String[][] {
{ "id",
"getId", "setId" },
{ "name", "getName", "setName" },
});
private int id;
private String name;
public int getId() { return id; }
public String getName() { return name; }
public void setId(int val) { id = val; }
public void setName(String val) { name =
val; }
// to be continued
}
|
The desriptor red describes the mapping of attributes from
the Java class Customer to the columns of the database table customer.
The constructor gets passed the following specifications
- The Java class the descriptor refers to
- The database table name, the descriptor maps the Java objects to
- A base class descriptor if existing
- A 2-dimensional string array with one entry per attribute/column:
- Name of the database table column
- Name of the get-method used to read the corresponding value
from the entity object
- Name of the set-method used to write a value to the entity
object
The mapping of an entity class to a database table is based on the
existence of public read and write methods for all attributes which are
supposed to be persisted. An entity class' mapping descriptor may
simply be defined
as a static class variable of the entity class itself. It is
recommended
to define the descriptor as a protected member so that it can be
referred
from derived entity types. Using the base class MappedObject
is
not mandatory here, but it provides a set of ready-to-use database read
and
write operations. Synchronization with the database is not performed
automatically
but requires explicite calls of appropriate member functions. The
following
extension of the class above shows how to use the standard operations
provided
by MappedObject to define a persistence constructor
and a
reconstructor. A persistence constructor immediately stores an
entity
in the database on construction. A reconstructor restores an entity's
data
by key from the database when the entitiy is created. The notions persistence
constructor and reconstructor are used with the meaning
above
in this documentation.
class Customer extends MappedObject {
// attributes and descriptor like above
// provide descriptor access
protected RecordDescriptor getDescriptor()
{ return red; }
// typical persistent construction
public Customer(int id, String name)
throws SQLException {
setId(id);
setName(name);
create();
}
// typical reconstruction
public Customer(int id) throws
SQLException {
setId(id);
find();
}
}
|
Find, update and deletion functions for an existing record require a
primary key to identify the record in the database. By default, the
methods MappedObject.update()
and MappedObject.delete()
assume that the first database field in the entity's mapping descriptor
makes
up the primary key. If this rule is not applicable, an alternative
specification
can be provided by overriding the function getKeyFields().
The following example illustrates the required extensions for such a
case:
class Customer extends MappedObject {
// attributes and descriptor like above
// primary key
protected static String[] pk = { "id" };
// primary key access function
public String[] getKeyFields() { return
pk; }
}
|
Generating entity types
Entity types with a canonic 1:1 mapping for existing database tables
can be generated by a simple generator. The generator is located in
package de.mathema.pride.util
and is called by the following command:
- <driver>
Name of the database driver class, e.g. sun.jdbc.odbc.JdbcOdbcDriver
- <db>
Name/URL of the database to access, e.g. jdbc:odbc:mydb
- <user>
Database login name
- <password>
Password for the login name specified above
- <table>,<table2>,...
Name(s) of the table(s) for which to generate an entity class for.
Specifiying multiple tables is required for the mapping of joins (see
also PriDE-Patterns-Join.html).
- <class name>
Optional name of the entity class to generate. By default, the name of
the database table is used.
- -h or -b or
<bean class name>
Optional specification of the generation mode. Option -h
generates a hybrid entity type as in the examples above, including both
a record descriptor and the data members. Option -b generates a
pure value type, including only the data members. In any other case,
there is a pure mapping type generated assuming the data to be stored
in a value type specified as parameter. See PriDE-Patterns-Decoupling.html
for further details. Default is -h.
- <base class name>
Optional name of a base class from which to derive the generated one.
Data members and mappings are only generated for databaes fields which
are not already mapped by the base class. The specified class must have
been already compiled and must be available from the class path. When
generating a value type only (option -b, see above), the
corresponding base adapter type must be specified.
When using the Eclipse 2.x platform, the generatror can also be run
from within the IDE by installing the PriDE
plugin.
Standard operations
Entity types derived from MappedObject,
inherit a set of standard operations for creating, reading, updating
and deleting records in the database. The same operations are available
in the ValueObjectAdapter
suitable for mapping pure value objects like ordinary Java Beans
instead of itself. The most important operations are roughly
explained in a few examples below.
Creation
The member function create()
allows to create a database record. It reads the contents of an entity
object and assembles an SQL insert statement according to the object's
mapping
descriptor to transmit the data to the database. PriDE's internal
operations
on record creation are illustrated in a UML diagramm (sequence view). Before calling
the
operation, the object's attributes must be reasonably initialized e.g.
concerning
- NULL-value constraints
- Unique constraints (especially the primary key if existing)
- Validity of foreign keys
- Value range limitations
Entity types created by PriDE's generator tool (s.a.) for example, map
database columns to primitive data types if the columns don't accept
NULL values.
This technically elliminates the risk of setting these attributes to null
and thus elliminates a typical source for mistakes. Nevertheless it is
the application's responsibility to also fulfill all other constraints
as well. If an error occurs on record creation, the creation method
forwards any SQLExceptions
to the caller. Any other exceptions are handled by a central error
handler (see chapter Error handling).
The following examples demonstrates the creation of an entity of type Customer
according to the examples above.
try {
new Customer(01, "Lessner");
}
catch(SQLException x) {
x.printStackTrace();
} |
Read/Query
Reading a particular instance by its primary key is performed by member
function find().
Overriding method getKeyFields()
allows an entity type to declare the database fields making up the
primary key. By default, the very first attribute in the entity's RecordDescriptor
is interpreted as primary key. This is often a good guess as many
databases use artificial technical keys for all tables.
Reading a record from the database usually follows a query-by-example
approach. An entity is created and the primary key attributes are set.
Afterwards
the find() method is called which assembles an SQL query from
an
AND conjunction of all key attribute values. The result is put in the
same
entity i.e. all other but the key attributes are supplemented from the
query's
result set. PriDE's internal operations on record retrieval are
illustrated
in a UML diagramm (sequence view). As in all other
database operations, any database errors are forwarded as SQLExceptions
to the caller and all other exceptions are handled by a central error
handler. The following example shows how to read an entity of type Customer.
try {
Customer c = new Customer();
c.setId(01);
c.find();
System.out.println(c.getName());
}
catch(NoResultsException nrx) {
System.out.println("no such customer");
}
catch(SQLException sqlx) {
sqlx.printStackTrace();
} |
An unsuccessful database query in this connection must normally be
understood as an error condition and is therefore reported by the find()
function as a NoResultsException.
As it is a derivation from SQLException, appropriate catch
blocks must always be defined before the one for
SQLExpression. However, a special treatment can often be omitted
anyway. Querying single objects by
key attributes is often encapsulated in the entity types' constructors
as
shown above for Customer class' reconstructor. The following example
shows the same query operation as above in a slightly simplified form:
try {
Customer c = new Customer(01);
System.out.println(c.getName());
}
catch(SQLException sqlx) {
sqlx.printStackTrace();
} |
A query-by-example can also be performed on any other combination of
columns, using function find(String[]).
The entity always gets passed the data of the very first matching
database record. If all matching records are of interest, the methods query(String[])
or wildcard(String[])
must be used. They also store the very first matching record's data in
the entity but additionally return a ResultIterator,
which allows iterative processing of all matching records. The
following example shows an iteration over all Customers with
a name starting with 'L':
try {
Customer c = new Customer();
c.setName("L%");
ResultIterator iter = c.wildcard(new
String[] { "name" });
do {
System.out.println(c.getName());
}
while(iter.next());
}
catch(NoResultsException nrx) { /* Nothing to do */ }
catch(SQLException sqlx) {
sqlx.printStackTrace();
} |
The functions toArray()
and toArrayList()
allow to fetch the complete query result as a whole, which is
comfortable for the processing of small result sets. Additional
variants for complex query operations can be found in the pattern overview.
Updates
The method update()
allows updating of existing database records. Similar to the record
creation, the attributes of an entity object are read to assemble an
SQL update statement. As a difference, only those attributes are
updated which are not part of the primary key. The latter ones are used
to assemble a where-clause to identify the record in the database.
PriDE's internal operations on record update
are illustrated in a UML diagramm
(sequence view). The
following
example shows an update for the famous Customer class from
the
examples above.
try {
Customer c = new Customer(01);
System.out.println(c.getName());
c.setName(c.getName() + "foo");
c.update();
}
catch(SQLException sqlx) {
sqlx.printStackTrace();
} |
Like for query operations, the method update(String[])
also allows updating multiple records in one statement by specifying
another combination of identifying attributes. Of course, this function
must only be used with great care. In addition it is often required to
update only selected attributes when addressing mutiple
records. Therefore the function update(String[],
String[]) is usually of more interest, as it allows to
specifiy both, the key fields and the fields to update.
Deletion
Similar to update calls, the function delete()
uses an entity's primary key attributes as identification of the record
which
should be removed from the database. Also deletion can be performed on
multiple
objects in one operation, using function delete(String[]).
PriDE's internal operations on record deletion are illustrated in a UML diagramm (sequence view). The following
example demonstrates record deletion for a Customer entity:
try {
Customer c = new Customer();
c.setId(01);
c.delete();
}
catch(SQLException sqlx) {
sqlx.printStackTrace();
} |
Most of the operations demonstrated above cause a database
manipulation. If the database connection in use does not apply auto
commitment, the operations require an explicite commitment of the
current transaction before the changes become durable. The ResourceAccessorJ2SE
and ResourceAccessorJ2SE
switch off the default auto commitment used by most database drivers.
The following section gives a short insight to PriDE's transaction
handling.
Transaction handling
Explicit transaction handling should only be performed in J2SE
applications. In application servers it is recommended to use container-managed
transactions, to keep the responsibility for transaction handling
out of the application code. Even in J2SE applications, it is helpful
not to spread explicit transaction handling all over the code but keep
it in one dedicated layer of a structured software architecture.
Running transactions in J2SE applications are terminated by the
functions Database.commit()
and Database.rollback().
The corresponding functions in MappedObject resp. ValueObjectMapper
have the same meaning, i.e. they are not limited to the object they are
applied
to. The termination of a transaction refers to the database connection
of
the current thread. A unique association of database connections to
application threads is managed transparently by the ResourceAccessorJ2SE
when calling Database.getConnection(). An explicit invokation
of this function is not required as it is implicitely called as soon as
there is any database interaction performed. The following code is a
minimal example for transaction handling in PriDE:
Customer customer = new Customer(01, "Lessner"); //
Creation
customer = new Customer(01); // Retrieval
System.out.println(customer.getName());
DatabaseFactory.getDatabase().commit();
// Alternatively: customer.commit(); |
Components which must be informed about the termination of
transactions can be registered as TransactionListener.
This is required e.g. to synchronize database-related caches. The
following example illustrates the definition of such a listener:
class TxListener implements TransactionListener {
public TxListener() {
DatabaseFactory.getDatabase().addListener(this);
}
public void commit(TransactionEvent e) {
// Do something }
public void rollback(TransactionEvent e) {
// Do something }
} |
Error handling
By default PriDE provides a central error handling which is used for
all exceptions not being of type java.sql.SQLException.
SQLExceptions propagated to the caller who is supposed to handle it
reasonably and potentially case-specific. Any other exceptions indicate
bugs in PriDE or application errors which usually don't allow to
continue the work in a reasonable way (e.g. a miss-defined RecordDescriptor
may cause a java.lang.IllegalAccessException to be thrown). A
centralized handling of these cases keep the throws-clauses of the
PriDE API clear.
Objects of type Database get passed an ExceptionListener
in the constructor which implements the methods process()
and processSevere().
The method process() is called in case of an error which
cancels the current operation but does not compromize the application
as a whole. The method processSevere() handles
exceptions, which indicate a severe error in PriDE or the application
code with the risk of loosing system integrity. PriDE currently only
uses processSevere() inside. the function is not supposed to
return control to the caller. i.e. it must either terminate the
application or throw a runtime exception.
The ExceptionListener to use can be changed with the
function DatabaseFactory.setDefaultExceptionListener().
The standard handling prints a stack trace of the processed exception
and terminates the application with exit code 1. Function process()
performs no handling at all, i.e. the passed exception is rethrown.
Substituting this listener is reasonable if the default behaviour does
not meet the expectations in a particular project, e.g. if the handling
must be unified with an application-specific scheme for logging or
exception propagation and reporting.
Accessing multiple
databases
The section above concerning PriDE initialization already explained the
operations required to make a single database accessible. Multiple
databases or different types of connections to the same database can be
added with the
context concept. A context is identified by a name and makes up
the
combination of a database name, a suitable ResourceAccessor
and
optionally an associated ExceptionListener. The
initialization operations
introduced so far, defined the details of the default context which is
used,
if nothing else is specified. The function DatabaseFactory.setContext(String)
allows to change the context. The first call of setContext
with a so far undefined context name causes an implicit creation of a
new context which can be initialized in the same way as already
explained. The following example demonstrates a simple creation and
usage of multiple contexts:
// Initialization of the default context
ResourceAccessorJ2SE accessor = new ResourceAccessorJ2SE
("sun.jdbc.odbc.JdbcOdbcDriver", "sa", "",
"sql.log");
DatabaseFactory.setResourceAccessor(accessor);
DatabaseFactory.setDatabaseName("jdbc:odbc:mydb");
// Creation and initialization of a second context
// See ResourceAccessorJ2SE Javadocs for performance
considerations
DatabaseFactory.setContext("second");
DatabaseFactory.setResourceAccessor(accessor);
DatabaseFactory.setDatabaseName("jdbc:odbc:mydb2");
// Fetch a record, now refering to second context
Customer c = new Customer(8400719);
// Switch to default context and store it in there
DatabaseFactory.setContext(DatabaseFactory.DEFAULT_CONTEXT);
c.create();
|
Beside an explicite programmatic change, the context may also be
switched based on entity types. If different contexts are used for
different databases, the schemata in these databases are usually also
different and thus require different entity types for access. For this
case there is an extended
constructor of RecordDescriptor available, which gets passed the
declaration of a context to refer to. The selection of the database is
done implicitely and type-driven within the functions of class ObjectMapper
and its
derivations. The following example shows two entity types which by
definition are associated to different contexts:
// Entity A associated with default context
class A extends MappedObject {
protected static RecordDescriptor red =
new RecordDescriptor
(Customer.class,
DatabaseFactory.DEFAULT_CONTEXT, "A", null,
new
String[][] { { "id", "getId", "setId" } });
// to be continued
}
// Entity B associated with context "second"
class B extends MappedObject {
protected static RecordDescriptor red =
new RecordDescriptor
(Customer.class,
"second", "B", null,
new
String[][] { { "id", "getId", "setId" } });
public B(A a) { setId(a.getId()); }
// to be continued
}
|
An explicite context switch is not required any more which
simplifies the
application code and reduces the risk of applying wrong contexts.
// Initialization of the default context
ResourceAccessorJ2SE accessor = new ResourceAccessorJ2SE
("sun.jdbc.odbc.JdbcOdbcDriver", "sa", "",
"sql.log");
DatabaseFactory.setResourceAccessor(accessor);
DatabaseFactory.setDatabaseName("jdbc:odbc:mydb");
// Creation and initialization of a second context
DatabaseFactory.setContext("second");
DatabaseFactory.setResourceAccessor(accessor);
DatabaseFactory.setDatabaseName("jdbc:odbc:mydb2");
// Fetch a record, implicitely using the default context
A a = new A(8400719);
// Write a record, implicitely using the second context
B b = new B(a);
b.create();
|
Prepared Statements
PriDE's standard operations are all based on plain SQL and work without
prepared statements. However, there is also rudimentary support for
that in
Version 2.0. The classes PreparedInsert
and PreparedUpdate
are available for the most popular types of prepared statements for the
creation
and the modification of database records. Additional helper classes may
be
derived from the common base class PreparedOperation
following the same definition scheme. The concept may be changed in
future versions of PriDE. The usage of prepared statements within PriDE
is illustrated in the following example, using PreparedInsert
for an optimized creation of Customer records:
Customer customer = new Customer(01, "lessner");
PreparedOperation pop = new
PreparedInsert(customer.getDesc());
pop.execute(customer);
customer.setId(02);
pop.execute(customer);
customer.setId(03);
pop.execute(customer);
db.commit(); |
Stored Procedures
For a simplified invocation of stored procedures, PriDE provides the
abstract base class StoredProcedure.
For any required concrete SP there is a corresponding caller class
derived from StoredProcedure, defining the call parameters as
public members. Input parameters are defined as final members which
therefore must be initialized in the constructor. Output parameters are
declared as non-final members. The execution is performed by calling StoredProcedure.execute(),
which causes PriDE to dynamically assemble a call string and associate
the object's members with the SP parameters. The following example
shows such a caller class for the stored procedure "CreateCustomer",
which
is supposed to create Customer records. If the SP has a different name
than
the class, the function getName()
must be overridden.
// Accessor class for Stored Procedure "CreateCustomer"
public class CreateCustomer extends StoredProcedure {
// Output parameter, is supposed to be generated by
the SP here
public int id;
// Input parameter to be provided by the caller
public final String name;
public CreateCustomer(String name) {
// This is a must, otherwise the compiler
will complain
this.name = name;
}
}
// Usage
StoredProcedure sp = new CreateCustomer("Lessner");
sp.execute(DatabaseFactory.getDatabase());
|
Like for entity types it were helpful to generate the access
classes. Unfortunately
the structure of meta data for SPs is strongly vendor-specific in the
database.
For Oracle databases there is a generator available in package de.mathema.pride.util
which is called by the following command line:
- -p <package>
Optional name of the database package (not Java package!), containing
the stored procedure - -d <driver>
Name of the driver class, e.g. sun.jdbc.odbc.JdbcOdbcDriver -
-i <db>
Name/URL of the database, e.g. jdbc:odbc:mydb - -u
<user>
Database login name - -w <password>
Password for the login name above - -s <sp>
Name of the stored procedure, which to generate an access class for -
-c <class>
Optional name of the access class to generate. By default, the name of
the SP is used
Further reading
For efficient usage of PriDE in more complex systems, it is stronly
recommended to read through the patterns
chapter which explains solutions for common problems in persistence
management not directly being covered by the PriDE standard
functionality.
For an overview of PriDE's standard attribute type mappings and
extension facilities, see chapter PriDE-Mapping.html.
An extended option for the mapping of entities are generic attributes. This features
hasn't evolved for a while now and therefore tends to be a little out
of date.
For the assembly of complex queries, there exists a simple expression builder.