Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Database

Object-Relational Mapping in Java with SimpleORM


December, 2005: Object-Relational Mapping in Java with SimpleORM

Martin and Ted are senior technologists at Wingspan Technology. They can be reached at [email protected].


Object-relational mapping encompasses a wide range of techniques and libraries for handling bidirectional serialization of runtime objects in an object-oriented system within a relational database. As is often the case with Java, a number of options are available, many of them open source. In this article, we examine a mature alternative named SimpleORM (http://www.simpleorm.org/), which was created and is maintained by Anthony Berglas.

We have adopted SimpleORM as our primary database integration library for a number of reasons:

  • SimpleORM acts as a direct conduit to the underlying JDBC data source. While there are internal caching mechanisms for performance reasons, there are no intermediate storage mechanisms visible to the user of the library. SimpleORM operations have the effect of acting directly on the underlying database.
  • SimpleORM lives up to its name in terms of providing well-defined functionality in a small and clear package. The simpleorm.jar weighs in at 112 KB and has no dependencies. The implementation easily supports the web site's claims that it is a practical option for developers to step into the library code to diagnose and fix any issues they might have.
  • SimpleORM is easy to configure, requiring few lines of code and no XML.
  • SimpleORM doesn't do much. This may sound like a strange thing to desire in an integration library, but in our case we appreciated SimpleORM doing what was required in a direct manner and leaving the rest to us.
  • SimpleORM can be compiled under both Java and J# (.NET). We wouldn't expect this feature to be of particular importance in most cases, but we were able to take advantage of it in our own development on one project.

To show how to use SimpleORM, we created a sample "Settings Storage" component. The source code for this component is available electronically (see "Resource Center," page 4) and is part of the (upcoming) 2.19 release of SimpleORM. Our settings component uses a representation similar to a filesystem or the Windows Registry. Logically, the only type of object in the system is a folder. A folder can contain subfolders and properties. Properties are simple name/value pairs where the value can be any serializable object in Java. Infinitely deep folder hierarchies are supported, subject to database constraints.

Data Model Authority

One of the most important elements of an Object-Relational Mapping tool is the location and specification of the data model to be used. In any Object-Relational Mapping (ORM) system, there will be at least two models—one or more table schemas in the database, and one or more class definitions in the OO environment. There can potentially be more, especially if the definition of the data models and mappings is external to both the programming language and database. The best case for any ORM is when the data-model authority is used to generate everything else. While this is easily achieved for new applications, it is often impossible when integrating with an existing system or code base.

In SimpleORM, the authority is built within the Java classes themselves. If desired, this mapping can be used to generate database tables or the mapping can be described in such a manner as to map to an already existing table. Users will immediately note how no effort is made to shield the user from database concepts. This, in part, illustrates the intention of SimpleORM to act as a direct conduit between the caller and the database.

Listing One shows two simple SimpleORM objects. SimpleORM objects inherit from SRecordInstance and include an SRecordMeta object. SRecordMeta objects are the heart of the SimpleORM system and contain the mappings used at runtime to present rows in database tables or queries as Java. By convention, the SRecordMeta is typically defined as a static member of the Java class it represents. This is not a strict requirement, but it does enable certain conveniences and there is no compelling reason to do otherwise.

Next we define the fields of the object (and columns of the corresponding table). Note that each field constructor takes the SRecordMeta as its first argument. The fields automatically register themselves with the SRecordMeta in their constructors, and are registered in the order they are defined in the source file. Order is generally not important, though all primary key columns must appear before all others. At least one primary key field is required for all objects as SimpleORM requires that all objects be uniquely identifiable by the system.

SRecordInstance is an abstract class with one abstract method getMeta(). Given an SRecordMeta, it is trivial to determine the Java class associated with it. Using this method, it is trivial to perform the reverse operation—to determine which SRecordMeta is bound to an arbitrary SimpleORM object instance. No reflection is required.

Further examination of Listing One illustrates a handful of SimpleORM mechanisms. Each data type in the system is represented by a different SField* class. All major data types are supported by SimpleORM, and it is trivial to add additional user-defined SField definitions, though out of the scope of this article.

Various flags are available to modify the field mappings. The SFD_GENERATED_KEY mapping indicates that the database should be used to generated values for the specified field. This only works for Integer columns and its implementation is database dependent. SimpleORM does not currently support optimal implementations for many databases (PostgreSQL is one for which it supports this mechanism very well), but this facility is the target of significant work in the upcoming release. The SFD_MANDATORY flag is used to indicate that NULL values should not be accepted for this field. There are of course many other options available.

The SFieldReference field mapping deserves special mention, as it is used to represent foreign key references in the database. SimpleORM supports an elegant mechanism for traversing foreign key references.

Connection Management & Transactions

Because of the various environments where SimpleORM might be used, creation of connections is deferred to the caller. This means that the caller is free to use any internal or external connection allocation or pooling mechanism. Such a mechanism might be provided as part of an application framework or J2EE server or can be trivially created.

Listing Two demonstrates the steps involved in initializing and terminating SimpleORM using the vanilla JDBC to create a database connection. Static methods on the SConnection class are used to bind the connection to the SimpleORM library, control a transaction, and release the connection. Internally, SimpleORM binds the JDBC connection to the current thread, meaning that all of the code elements in Listing Two must be called by the same thread. Any SimpleORM invocations made by that thread in the interior of the requested transaction is automatically routed to the appropriate connection.

Though it may not appear so at first glance, this mechanism is thread safe due to SimpleORM's internal usage of Thread Local Storage mechanisms. In fact, because of those mechanisms, SimpleORM is able to defer almost all locking mechanisms to the database itself. Simultaneous threads executing within SimpleORM each have their own connections and transactions; they each execute within their own operating contexts.

Object Management

Database-mapped objects in SimpleORM are all handled through the SRecordInstance object interface. This interface can be extended via subclassing if desired, but it is entirely possible to utilize SimpleORM with nothing more than Listing One.

Object creation and loading is handled via a single method on SRecordMeta, findOrCreate(). FindOrCreate takes as its parameter a single object or an object array representing the primary key value(s) of the object being requested. If the primary key is designated to be generated by the database, then new objects can be created with the createWithGeneratedKey() interface.

Listing Three demonstrates some common interactions we might perform on our example Folder object. We begin by loading a folder with an ID value of 5 and creating a second folder with a newly generated ID. We can either cast these objects to be our defined type Folder, or deal with them as generic SRecordInstance objects.

We then go on to request the name of folder1 and the parent of folder2. Note how we use specific, type-based get routines of the SRecordInstance class. These routines provide additional data validation both in ensuring that the SField instance is of the appropriate type and is valid in the current transaction and also that the value retrieved from the database adheres to the declared expectation. In the case of a foreign-key reference, the referenced object is automatically loaded and returned, or retrieved from the transaction cache if possible. For most primitive data types, we can retrieve the data any number of ways and SimpleORM will coerce the actual value into the requested type. We do this with the ID field, requesting it as three different data types.

It is also trivial to define and use methods such as getName() if that is preferred as a matter of style, but there is no need to do so. When data is requested from the database (such as during the object load), the attached connection is immediately used to retrieve the appropriate values. This data is then loaded into the SRecordInstance-derived object. Unless specifically directed otherwise, SimpleORM never reloads a data value during a single transaction, so repeated requests for the same data return the value cached in the SRecordInstance and do not touch the connection a second time.

For this reason, SRecordInstance objects are only valid within the context of the SimpleORM transaction that created them. This aspect initially caused us great discomfort and can, for some developers, force you to rethink how your application should interact with the database. It is the nature of many developers to think of the database as a storage mechanism like a filesystem. Data objects are saved and loaded from the database and an ORM simply facilitates that process. SimpleORM takes a different approach, where you are either talking to the database or you are not. The data lives only in the database, and you use SimpleORM as a conduit to facilitate that communication. During our own education, we found that understanding both models leads to a much deeper understanding of ORM technology in general. SimpleORM is a great choice if you want your application to directly manipulate your database.

There are parallel set routines for all of the get routines with the same type signatures. Set operations manipulate the object cache immediately, and the database is updated en-masse when a transaction is committed or when SConnection.flush() is called. A unique object in the database is only ever loaded into the object cache for a transaction once. This means that if you load an object, navigate to it via foreign-key references, and retrieve it from a query's result set, the exact same object instance will be returned each time.

Object deletion is accomplished by calling the deleteRecord method on any SRecordInstance object. This method can be overridden as in Listing Four so long as the base implementation is invoked. This would typically be done to prevent any foreign-key violations that might result from deleting an object.

It is always faster to delete or update several related records at once using a single SQL Delete statement than to fetch them into memory using JDBC and delete them one by one. SimpleORM supports this by enabling records for a specific table to be purged from memory, and so maintaining a consistent view of the data. (Most databases also support Cascade Delete options, but they can be very confusing for both Java and stored procedure code.)

Queries

Listing Five illustrates usage of the SimpleORM query API to iterate over all folder objects at the root of our hierarchy. The newQuery method on SRecordMeta creates a new query object designed to return objects of the type represented by SRecordMeta. There are a great number of operators available, including the isNull method we use here. There are also numerous type-specific gt (greater than), lt (less than), and eq (equals) methods. Conjunction operations (and and or) are available as well. The eq method can directly support references to other tables, and more general table joins are also supported, but our example is not well suited to demonstrate this advanced functionality.

Listing Six demonstrates another usage of the query API. In this case, our queries are designed to return exactly 0 or 1 records. Note how isNull, and, and eq are chained together mimicking the equivalent SQL. The SimpleORM query API is an extremely thin layer on top of SQL. This mechanism lends itself to rapid adoption by developers experienced with SQL queries.

It is also possible to manually specify the SQL fragment that forms part of the where clause, which is occasionally useful for less common or database specific queries. A further mechanism enables the entire SQL query to be specified with the cache being flushed manually if needed.

Rules Enforcement

SimpleORM supports rules enforcement via a series of overridable methods on the SRecordInstance class. By far, the most common use of this model is in the area of field validation. validateField is called each time a field value is set. validateRecord is called once immediately before an object is written to the database. A violation is signaled by throwing an SValidationException.

In practice, it is better to use validateField wherever possible, as this will cause an exception to be thrown at the exact location where the offense is committed. validateRecord is called only when SConnection.flush() is called, or when a transaction is committed. As a result, it may not be immediately obvious when the infraction occurred. Of course, in many cases, per-field validation is not sufficient. For example, the possible values for one field might be dependent on the setting of another field. To avoid chicken-and-egg type problems, it is better to simply validate the entire record at once rather than try to do it piecemeal as the fields are being set.

Listing Seven shows a simple validation routine applied to the Folder's name field. Here we see one of the benefits of having our ORM metadata (SRecordMeta and SField* entries) defined as static members of the object implementation (SRecordInstance). With a simple reference equality check we can quickly determine which field is being modified and apply whatever logic we wish. Because we defined this field with the SFD_MANDATORY flag, null values will not be permitted. That enforcement is only done when the object is written to the database at the end of a transaction, so we can add an additional check here to provide for a more timely error. In our full implementation of the example, we wish to be able to represent a folder hierarchy with a path string. Blank folder names and folder names containing our path delimiter character make parsing a path string more difficult, so we can simply disallow them here.

Conclusion

SimpleORM is a lightweight, yet powerful ORM implementation available under a liberal open-source license. SimpleORM makes little, if any, attempt to shield the user from database concepts, so developers with existing database knowledge and experience will have more success when using the library. The code base is similarly lightweight and easy to extend, though for most purposes, this will not be necessary.

DDJ



Listing One

public static class Folder extends SRecordInstance
{
  static SRecordMeta s_meta = new SRecordMeta(Folder.class, "FOLDER");

  static SFieldInteger ID = new SFieldInteger(s_meta,"ID",new SPropertyValue[]
{ SSimpleORMProperties.SFD_PRIMARY_KEY, 
                                  SSimpleORMProperties.SFD_GENERATED_KEY });
  static SFieldReference PARENT = new SFieldReference(s_meta,s_meta,"PARENT");
  static SFieldString NAME = new SFieldString(s_meta, "NAME", 64,
                                  SSimpleORMProperties.SFD_MANDATORY);
  public SRecordMeta getMeta() { return s_meta; }
}
public static class Property extends SrecordInstance
{
  static SRecordMeta s_meta = new SRecordMeta(Property.class, "PROPERTY");
  static SFieldReference PARENT = 
     new SFieldReference(s_meta, Folder.s_meta, "PARENT", new SPropertyValue[]
     { SSimpleORMProperties.SFD_PRIMARY_KEY, 
          SSimpleORMProperties.SFD_MANDATORY });
  static SFieldString NAME = new SFieldString(s_meta, "NAME", 64,
          new SpropertyValue[]
         { SSimpleORMProperties.SFD_PRIMARY_KEY, 
           SSimpleORMProperties.SFD_MANDATORY });
  static SFieldObject VALUE = new SFieldObject(s_meta, "VALUE");
  public SRecordMeta getMeta() { return s_meta; }
}
Back to article


Listing Two
Connection conn = DriverManager.getConnection("jdbcURL", "user", "pass");
SConnection.attach(conn, "Connection Name");
SConnection.begin();
 ...
SConnection.commit();
SConnection.detachAndClose();
Back to article


Listing Three
Folder folder1 = (Folder)Folder.s_meta.findOrCreate(new Integer(5));
Folder1.assertNotNewRow();
SRecordInstance folder2 = Folder.s_meta.createWithGeneratedKey();
String name = folder1.getString(Folder.NAME);
Folder parent = (Folder)folder2.getReference(Folder.PARENT);

Object idObject = folder2.getObject(Folder.ID);
String idString = folder2.getString(Folder.ID);
Int idInt = folder2.getInt(Folder.ID);

folder1.deleteRecord();
Back to article


Listing Four
public static class Folder extends SRecordInstance
{
  ...
  public void deleteRecord()
  {
    // Delete all subfolders
    SResultSet subFolders = s_meta.newQuery().eq(PARENT, this).execute();
    while (subFolders.hasNext())
      subFolders.getRecord().deleteRecord();

    // Delete all properties of this folder
    SResultSet props = 
            Property.s_meta.newQuery().eq(Property.PARENT, this).execute();
    while (props .hasNext())
      props.getRecord().deleteRecord();
    // Invoke the base implementation
    super.deleteRecord();
  }
Back to article


Listing Five
SQuery query = Folder.s_meta.newQuery().isNull(Folder.PARENT).
                                                      ascending(Folder.NAME);
SResultSet rs = query.execute();
while (rs.hasNext)
{
    Folder folder = (Folder)rs.getRecord();
    ...
}
Back to article


Listing Six
SQuery query = Folder.s_meta.newQuery()
                .isNull(Folder.PARENT)
                .and()
                .eq(Folder.NAME, "Test");
SResultSet rs = query.execute();
Folder folder = (Folder)rs.getOnlyRecord();
Back to article


Listing Seven
public static class Folder extends SRecordInstance
{
  ...
  public static final String DELIMETER = "/";
  public void validateField(SFieldMeta field, Object newValue)
  {
    if (NAME == field)
    {
      if (null == newValue ||
          "".equals(newValue) ||
         (newValue.toString().indexOf(DELIMETER) >= 0))
      {
        throw new SValidationException("Folder name cannot be blank or 
                    contain " + "the DELIMETER character: " + DELIMETER);
      }
    }
  }
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.