DekGenius.com
[ Team LiB ] Previous Section Next Section

8.3 Object-Relational Mappings

So far in this chapter, we've assumed that the table structures for the business objects we're working with are known in advance. This is generally true when adapting legacy systems or coming into a project mid-way, but not when starting from scratch

When building new applications with a persistent backend (and that's pretty much always, whether a database is involved or not), you obviously need to design the persistence approach. Persistence models are much harder to change than other areas of an application, particularly when a version of a system has been moved into production use. Data must be either abandoned, an option that is generally not smiled upon by senior management, or converted, which is time-consuming and error-prone. Although modern database tools have made the process much easier and more flexible than in the past, there's still lots of pressure to get it right the first time.

Developers extending an existing application have it both easier and harder than those working on something new. In an existing application, the philosophical approach to persistence has usually already been chosen, so developers adding new features don't have to reinvent that particular wheel. On the other hand, adding a substantial amount of new persistence functionality to an existing system can involve stretching existing tables and methodologies beyond their original design intent, and it always involves migration hassles. Attempts to minimize migration efforts often require the creation of entirely separate table structures alongside the existing ones, which leads to data structures that are difficult to maintain, query against, or even understand.

The rest of this chapter looks at three approaches for mapping objects into the persistence layer. Again, this generally means the database, although two of the approaches can be applied to disk-based approaches (with varying degrees of success), or even to more esoteric persistence layers like JavaSpaces implementations.

8.3.1 Complex Class Relations

Before diving into table inheritance, it's worth spending a little bit of additional time discussing how entities are related to one another in the database. Since direct relationships between tables are handled by storing primary and foreign keys, the number of columns available for storing foreign keys limits the relationships between rows in any two tables. For example, Figure 8-5 shows a simple approach to associating a main table with a subtable.

Figure 8-5. Two directly related tables
figs/j2ee_0805.gif

In the tables in Figure 8-6, it's impossible for one member to have more than one membership. If this is an acceptable limitation for the system, we don't have any problem. But what if want to keep track of people's older memberships, or allow someone to have multiple kinds of memberships (such as to the museum itself and to the Arty Films Club that meets every third Thursday)? One option is to have a row in the MEMBER table for each membership, but unless we know for sure that we're never going to have to add additional kinds of memberships in the future, that approach doesn't really accomplish what we want. What we want is to associate a single member with multiple memberships. To accomplish this goal, modify the table structure to include a "linking table" that stores multiple key relationships.

Figure 8-6. Two tables connected by a link table
figs/j2ee_0806.gif

This approach gives us the most completely normalized view of the data possible: no data is duplicated, queries are as simple as possible, and we have the flexibility to associate a member with multiple memberships.

8.3.2 Table Inheritance Pattern

In an ideal world, we want a database structure that reflects the object model while taking the greatest possible advantage of the underlying database. The Table Inheritance pattern involves creating database tables that reflect the class structures within the application. We'll look at two different strategies for implementing table inheritance.

Most applications use variations of the Table Inheritance pattern, although the degree to which the table structures match the class structures often depends on whether the data model predates the application, and on the designer's level of experience with RDBMS design. EJB CMP implementations, particularly when you allow your deployment tool to develop the database structures, often use variations on this approach.

Table inheritance allows you to make relatively full use of the underlying database to enforce integrity and allow external systems, such as report generators and EAI tools, to access your application's data. It has the disadvantage of tying your database structure to your class structure, imposing additional work whenever one or the other must change. When writing database access code, implementing table inheritance by hand can take longer than other approaches.

8.3.2.1 Concrete Table Inheritance strategy

The Concrete Table Inheritance strategy provides an object mapping to a relational database by defining a table for each concrete class in the object model, along with tables defining the relationships between objects. In the example below, we have a simple class structure used for issuing ID badges at a museum. Members get one type of badge, and employees get another. The object model includes an abstract Person class, which contains information common to employees and members, and concrete Member and Employee classes. Figure 8-7 illustrates the structure.

Figure 8-7. Simple abstract classes
figs/j2ee_0807.gif

Implementing concrete table inheritance for this structure gives us two tables, MEMBER and EMPLOYEE. Both tables contain the fields represented in the Person class, and also contain the additional fields present in the concrete implementing classes.

The advantage of concrete table inheritance is that it keeps the number of tables manageable, while still providing some separation, and the code is easy to understand. Keeping all the data required to load a class instance in a single row of a single table also provides the best performance possible: in the world of databases, there is nothing more efficient than a single-row retrieval from a table based on a primary key.

8.3.2.2 Class Table Inheritance strategy

The major disadvantage of concrete table inheritance is that the database has no equivalent to the relationship between the abstract parent class and the concrete classes. While performing a query across members and employees is possible, it is difficult and convoluted. The Class Table Inheritance strategy supplies an alternative to the Concrete Table Inheritance strategy by providing a table for each class in the inheritance hierarchy, whether it's abstract or not. This makes it easier to run queries against common elements while ensuring that we unambiguously manage all the data associated with each entity.

With this approach, we address the Person-Member-Employee relationship by creating three tables, PERSON, MEMBER, and EMPLOYEE, each containing the fields specified by the equivalent class, and linked via a primary key on PERSON referenced as a foreign key by MEMBER and EMPLOYEE. Populating an instance of the Employee or Member classes requires reading from the PERSON table and the appropriate subtable. At the cost of increased complexity, this approach eliminates duplication, allows us to easily perform queries across all persons, and even allows a single person to be both an employee and a member, if necessary.[5]

[5] Of course, things can get confusing here on the Java side, since the Java object model doesn't support multiple inheritance. To support this kind of multiple relationship in your code, you would likely have to instantiate both an Employee and a Member object for the person. The advantage is that you can use the same database and the same record in the PERSON table.

It's sometimes tempting to create a single large table containing every conceivable column, and use certain columns for certain types of records. This temptation should be resisted.

8.3.3 The Serialized Entity Pattern

Particularly in the early stages of development, sometimes we just need to build an easy-to-implement persistence system that incorporates the full richness of Java objects.

Many enterprise applications need to expose their underlying data to other systems: reporting and Enterprise Application Integration tools are two common examples. But not all applications do, either because an application is small, truly stand-alone, or because it provides its own interface to external systems, as is increasingly the case with web services. These applications don't need to worry about making their internal data externally intelligible.

The Serialized Entity pattern uses object serialization to store an object instance to persistent storage. In Java, this is done by implementing the Serializable interface, which allows an object and all of its fields to be written to a stream. The serialized object can be read back later and used to reconstruct the object in memory. Since serialization is stream-based, the objects can be stored directly on disk or within a database.

Serialized objects allow the object model to ensure data integrity across the application's complete data set via the usual Java programming practices. Since objects stored by reference in other objects are included in the serialization process, a complex object tree can be saved and restored in a single call, and changes to the data structure during development don't require any modifications to the persistence code.

The Serialized Entity pattern can be helpful during the development process. Implementing entity serialization is usually simple, particularly if it's done at a fairly high level in the object hierarchy. You can use serialization while your object develops, and switch over to implementing the Concrete Table Inheritance pattern or another approach once functional requirements have settled down. This strategy can be particularly effective in projects that use an iterative or test-driven design methodology, or in which requirements are in more flux than usual.

The pattern has two major disadvantages. Once stored in binary form, the objects are completely opaque to human inspection and to most if not all data analysis tools. Additionally, since the Java serialization method is based on the class itself, if the class signature changes, the serialized object becomes unusable. This means that when we're converting a production application, we must either write a conversion application that loads the old objects, converts them to the new object, and reserializes them, or develop a completely separate parallel transaction system that can be "played back" to rebuild the object model (see Chapter 10 for a little more discussion on transactions and auditing).

8.3.3.1 A serialized entity example

In this example, we'll serialize a DTO representing a museum member to the database and retrieve it. Here's the skeleton of the class we're persisting:

public class MemberDTO  {
  public long getMemberNumber(  ) {}
  public void setMemberNumber(long newMembernumber){}

  public String getFirstname(  ) {}
  public void setFirstname(String newFirstname) {}
  public String getLastname(  ) {}
  public void setLastname(String newLastname) {}
  public String getAddress1(  ) {}
  public void setAddress1(String newAddress1) {}
  public String getAddress2(  ) {}
  public void setAddress2(String newAddress2) {}
  public String getCity(  ) {}
  public void setCity(String newCity) {}
  public String getState(  ) {}
  public void setState(String newState) {} 
  public String getZip(  ) {}
  public void setZip(String newZip) {}
  public Long getFreePasses(  ) {}
  public Long setFreePasses(Long passes) {}
}

We'll leave out the concrete implementation for space (it can be downloaded with the rest of the code examples for this book). The table structure looks like this:

SQL> desc members;
 Name                                Null?    Type
 ----------------------------------- -------- -------
 MEMBER_NO                           NOT NULL NUMBER
 OBJECT_DATA                                  BLOB

The DAO object that uses this table structure is shown in Example 8-5. We include two methods: a finder to look up an object and a saver to persist it.

Example 8-5. MemberSerializableDAO.java
import java.io.*;
import java.sql.*;
import javax.naming.*;

public class MemberSerializedDAO  {

  public MemberSerializedDAO(  ) {
  }


  public MemberDTO findMember(long member_no)  {
    Connection con = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    MemberDTO member = null;
    
    try {
      con = getConnection(  );
      ps = con.prepareStatement(
        "select object_data from members where member_no = ?"); 
      ps.setLong(1, member_no);
      rs = ps.executeQuery(  );
      if(rs.next(  )) {
        ObjectInputStream ois = new ObjectInputStream(rs.getBinaryStream(1));
        member = (MemberDTO)ois.readObject(  );
        ois.close(  );
      }
      rs.close(  );
    } catch (ClassNotFoundException e) {
      e.printStackTrace(  );
    } catch (IOException e) {
      e.printStackTrace(  );
    } catch (SQLException e) {
      e.printStackTrace(  );
    } finally {
      if (ps != null)
        try { ps.close(  ); } catch (SQLException e) {}
      if (con != null)
        try { con.close(  ); } catch (SQLException e) {}
    }
    
    return member;
  }

  public void saveMember(MemberDTO member)  {
    Connection con = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    long memberNo = member.getMemberNumber(  );
    if(memberNo < 1) 
      return;
   
    try {
      con = getConnection(  );
      ps = con.prepareStatement("delete from members where member_no = ?");
      ps.setLong(1, memberNo);
      ps.executeUpdate(  );
      ps.close(  );

      ps = con.prepareStatement("insert into members " +
        "(member_no, object_data) values (?, ?)");
      ps.setLong(1, memberNo);
      ByteArrayOutputStream baos = new ByteArrayOutputStream(  );
      ObjectOutputStream oos = new ObjectOutputStream(baos);
      oos.writeObject(member);
      ps.setBytes(2, baos.toByteArray(  ));
      ps.executeUpdate(  );
      
    } catch (IOException e) {
      e.printStackTrace(  );
    } catch (SQLException e) {
      e.printStackTrace(  );
    } finally {
      if(ps != null) 
        try { ps.close(  ); } catch (SQLException e) {}
      if(con != null)
        try { con.close(  ); } catch (SQLException e) {}
    }
  }

  private Connection getConnection(  ) throws SQLException {
        try {
      Context jndiContext = new InitialContext(  );
      DataSource ds = (DataSource)
        jndiContext.lookup("java:comp/env/jdbc/DataChapterDS");
      return ds.getConnection(  );
    } catch (NamingException ne) {
        throw new SQLException (ne.getMessage(  ));
    }
  }
}
8.3.3.2 XML and serialized entities

The two major disadvantages of serialization can be overcome somewhat by using an XML-based serialization approach. There are a number of existing components that support XML serialization from Java objects, and if your implementation changes, it's easier to modify the existing data to load properly. XML creation and processing, though, are activities with high overhead.

Various technologies exist to make XML serialization easier, including the Java API for XML Binding (JAXB) and the Digester component available from the Jakarta project Commons library (http://jakarta.apache.org).

8.3.4 The Tuple Table Pattern

Serialized entities make it easy to change the structure of the objects in the application without having to adjust the persistence layer, but, as we mentioned, they have two major disadvantages: the persisted data is not human-readable, and existing data can be difficult to convert when the format changes. The Tuple Table pattern stores an object in the database in a highly flexible format that can be manipulated at the database level by normal human beings and can be easily extended without having to convert existing data.

Much of the data used in an enterprise application can be condensed into sets of fields. In simple applications, the fields can be stored as simple properties files, of the format:

name=value
othername=othervalue

And so forth. We can use this approach to persist objects to the database by assigning each object instance a primary key and then storing labeled values for each key (a tuple). Rather than storing an address in a table with an ADDRESS1 column, an ADDRESS2 column, a CITY column, and so on, we store it in a table with three columns: ADDRESS_KEY, FIELDNAME, and FIELDVALUE. An address might look like this in the database:

SQL> select address_key, fieldname, fieldvalue from addresses;

ADDRESS_KEY FIELDNAME    FIELDVALUE
----------- ------------ ----------------------------------------------------
       9325 Address1     10 Main St
       9325 City         Upper Norfolk
       9325 State        NY
       9325 Zip          20123

In order to work with the result data, we can load it in a hashtable or map the values to fields in an object. If we want to add an Address2 field, all we have to do is start adding that row to the database and update our code accordingly. If we write things properly, the database will be seamlessly upward- and backward-compatible.

The primary advantage of this approach is extreme flexibility. By storing object fields as name and value pairs, we can modify the application to include new fields without changing the underlying database structure. Existing data can be preserved without requiring complicated migration scripts. And unlike serialized entities, the data can be viewed, edited, and analyzed from within the database environment itself, even though the SQL used in the analysis process might be a bit more complex than with a more rigidly defined data model.

The Tuple Table pattern can also make it easier to customize applications to different environments. Many enterprise applications are deployed in more than one environment, and often the data managed will differ slightly between implementations. For example, a CRM system deployed to one customer might need to track five different types of data per customer. The same system, deployed at another customer, might need to track four of those five data points, plus two others (such as membership in a customer loyalty program or a willingness to receive additional sales calls). Storing the customer data in a tuple table means that even if the classes using the data need to be modified for each client, the underlying schema can remain the same.

The primary disadvantage of the Tuple Table pattern is in its integrity enforcement. Relational databases are very good at enforcing rules at the column level, but aren't so good at enforcing rules at the data level. It's easy to tell the database to ensure that the value of the IS_PREVIOUS_CUSTOMER column is "T" or "F", but much harder to tell it to ensure that the value of the VALUE column is "T" or "F" if and only if the NAME column is "PreviousCustomer".

Tuple tables also exact a performance penalty. When retrieving field data from a database table, retrieving a single row containing only the field data is much more efficient than retrieving several rows, each of which contains both a name and a value. In addition to the extra networking overhead between the object and the database, the object must also do more processing. As usual, the tradeoff is between scalability and flexibility.

There are space considerations as well: storing a label for each individual piece of data will result in a much larger database, although with modern disk arrays this is less of a concern than in years past.

8.3.4.1 A tuple table example

We can use Java reflection with a DAO object to retrieve an object from a tuple table or write it back to the table. By using reflection, we can retrieve the field names directly from the object we're persisting. Here's the structure of the table we'll use:

SQL> desc object_data;
 Name                                      Null?    Type
 ----------------------------------------- -------- -------------
 OBJ_PK                                    NOT NULL NUMBER
 FIELDNAME                                 NOT NULL VARCHAR2(20)
 NUMERICAL                                          NUMBER
 STRING                                             VARCHAR2(255)

The OBJECT_DATA table doesn't know about particular object types; it's counting on the application to ensure that no two objects share a primary key. The FIELDNAME field stores the name of the field. The value of the field is stored in the NUMERICAL or STRING fields, depending on type. If both are null, the field is assumed to be null. Having a value in both columns is considered to be an invalid state.

Just like in the previous example, Example 8-6 is going to load an object of MemberDTO, describing a museum member. The findMember( ) method creates a new MemberDTO object based on a provided primary key. The saveMember( ) method takes a MemberDTO and saves it to the database, using the brute-force approach of wiping out the rows in the table for that primary key and replacing them. Also, on the Java side, we've limited our property types to String and Long objects, although they can be extended easily. We've also included a main( ) method that you can use to see how the program works.

Example 8-6. MemberTupleDAO.java
import java.util.*;
import java.lang.reflect.*;
import java.sql.*;
import javax.sql.*;
import javax.naming.*;

public class MemberTupleDAO  {

   public MemberDTO findMember(long member_no)  {
    Connection con = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    MemberDTO member = new MemberDTO(  );
    member.setMemberNumber(member_no);
      
    try {
      con = getConnection(  );
      ps = con.prepareStatement("select fieldname, numerical, string " + 
        "from object_data where obj_pk = ?");
      ps.setLong(1, member_no);
      rs = ps.executeQuery(  );

      while(rs.next(  )) {
        String fieldName = rs.getString(1);
        String strVal = rs.getString(3);

        if(strVal != null)
          setVal(fieldName, member, strVal);
        else { 
          // We do this indirectly to make database typecasting more reliable
          long lngVal = rs.getLong(2);
          if(!rs.wasNull(  ))
            setVal(fieldName, member, new Long(lngVal));
        }
      }

      rs.close(  );
      ps.close(  );
    } catch (SQLException e) {
      e.printStackTrace(  );
      return null;
    } finally {
      if(con != null)
        try { con.close(  ); } catch (SQLException e) {}
    }
    
    return member;
   }
   

  public void saveMember(MemberDTO member)  {
    Connection con = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    long memberNo = member.getMemberNumber(  );
    if(memberNo < 1) 
      return;
      
    Class[] stringParam = new Class[] {String.class};
    Class[] longParam = new Class[] {Long.class};
    
    try {
      con = getConnection(  );
      ps = con.prepareStatement("delete from object_data where obj_pk = ?");
      ps.setLong(1, memberNo);
      ps.executeUpdate(  );
      ps.close(  );

      ps = con.prepareStatement("insert into object_data " +
        "(obj_pk, fieldname, numerical, string) values (?, ?, ?, ?)");
      ps.setLong(1, memberNo);

      Method[] methods = member.getClass().getMethods(  );
      for(int i=0; i < methods.length; i++) {
        String mName = methods[i].getName(  );
        if(mName.startsWith("get")) {
          try {
            if(methods[i].getReturnType(  ) == String.class) {
              ps.setString(2, mName.substring(3));
              ps.setNull(3, Types.NUMERIC);
              ps.setString(4, (String)methods[i].invoke(member, new Object[] {}));
              ps.executeUpdate(  );
            } else if (methods[i].getReturnType(  ) == Long.class) {
              ps.setString(2, mName.substring(3));
              ps.setObject(3, (Long)methods[i].invoke(member, new Object[] {}), 
                Types.NUMERIC);
              ps.setNull(4, Types.VARCHAR);
              ps.executeUpdate(  );
            } 
          } catch (IllegalAccessException e) {
            e.printStackTrace(  );
          }catch (InvocationTargetException e) {
            e.printStackTrace(  );
          }
        }
      }
      ps.close(  );
      
    } catch (SQLException e) {
      e.printStackTrace(  );
    } finally {
      if(ps != null) 
        try { ps.close(  ); } catch (SQLException e) {}
      if(con != null)
        try { con.close(  ); } catch (SQLException e) {}
    }
  }

  /**
   * Set a value on the target object, by searching for a set<fieldName> method
   * which takes a parameter of the same type as the "param" parameter.
   */
  private void setVal(String fieldName, Object target, Object param) {
    
    try { 
      Class targetClass = target.getClass(  );
      Method setter = targetClass.getMethod("set" + fieldName, 
                                            new Class[] { param.getClass(  ) });
      setter.invoke(target, new Object[] { param });
    } catch (NoSuchMethodException e) {
      // Ignore it - it must not be in the target
    } catch (InvocationTargetException e) {
    } catch (IllegalAccessException e) {
      e.printStackTrace(  );
    }
  }
  
   private Connection getConnection(  ) throws SQLException {
    try {
      Context jndiContext = new InitialContext(  );
      DataSource ds = (DataSource)
        jndiContext.lookup("java:comp/env/jdbc/DataChapterDS");
      return ds.getConnection(  );
    } catch (NamingException ne) {
        throw new SQLException (ne.getMessage(  ));
    }
  }

  public static void main(String [] args) {

    MemberTupleDAO mtd = new MemberTupleDAO(  );
    MemberDTO member = mtd.findMember(1);
    System.out.println(member.getFirstname() + " " + member.getLastname(  ));
    System.out.println(member.getFreePasses(  ));
    System.out.println(member.getCity(  ));
    System.out.println(member.getAddress1(  ));

    member.setMemberNumber(4);
    member.setFirstname("Reginald");
    mtd.saveMember(member);

    member = mtd.findMember(4);
    // Will display "Reginald" and the last name from the original member
        System.out.println(member.getFirstname() + " " + member.getLastname(  ));
  }
}

When using tuple tables and reflection, remember that if you change the structure of your objects, you'll have to update the database to include the new field names. This isn't much of a chore: converting data is generally just a simple SQL UPDATE statement, and after the development phase is over, your model shouldn't change much.

    [ Team LiB ] Previous Section Next Section