by Gerald Bachlmayr

Querying for persistent objects without a query language

news
Mar 20, 20069 mins

A powerful combination of Hibernate and JavaServer Faces

Permanent storage of information is a standard feature of software applications, and several methods and technologies for Java-based applications achieve such persistence. Each method has its advantages and tradeoffs. For example, querying for objects via SQL and Java Database Connectivity (JDBC) allows dynamic queries, but has the following downside: each query type (create, read, update, delete) must be maintained individually when the database scheme changes. In some persistence strategies, queries can be named and separated from the source code so all queries are in a centralized location.

Like other persistence frameworks, Hibernate has its own object-oriented query language and supports native SQL. In addition, it includes techniques for avoiding query language statements and for executing queries in an object-oriented way. These may be the preferred techniques for developers used to the object-oriented paradigm. The Hibernate criteria query API features two query types that allow programmatic fetching of persistent objects: query by criteria (QBC) and query by example (QBE). This article illustrates a powerful application of both query APIs (see Hibernate reference documentation for more information on the query APIs). Code examples include a Web-based query, with a full roundtrip from a JSP (JavaServer Pages) page to the persistence layer and back without any query language or select statements. This functionality is achieved by combining JavaServer Faces with Hibernate.

The domain model

Before digging deeper, let’s first examine the preconditions of this example’s use of the criteria query API.

The domain model in Figure 1 illustrates the two query techniques.

Figure 1. Overview of persistent POJOs. Click on thumbnail to view full-sized image.

The support for mapping definitions by Java annotations was still in beta when this article was written. Therefore, the mapping is defined in XML files, as shown in the listing below.

Listing 1. Hibernate mapping definition

 <hibernate-mapping>
  <class name="com.bachlmayr.hibcrit.pojo.Customer" 
      table="customer">
    <!-- further mapping definitions see source code -->
    <set name="contracts" table="customer_contract" 
          lazy="true" cascade="save-update">
    <key column="customerId"/>
    <many-to-many class="com.bachlmayr.hibcrit.pojo.Contract" 
          column="contractId"/>
    </set>
</hibernate-mapping>

Query by criteria

To use the criteria query API, the object mapping definition must be finalized. According to the programmatically defined criteria and restrictions, a result list is fetched from the database. A simple restriction of results can be achieved by defining a class corresponding to the expected objects. Criteria queries are polymorphic by default, meaning that, not only objects of the defined class are fetched, but also instances of its subclasses. The code example below queries objects of type Customer and receives objects of type BusinessCustomer and PrivateCustomer:

Listing 2. A simple query by criteria

 Criteria crit = session.createCriteria(Customer.class);
List result = crit.list();
for(Iterator <Customer>i=result.iterator(); i.hasNext();) {
    Customer c = i.next();
    assertTrue( (c instanceof BusinessCustomer) 
            || (c instanceof PrivateCustomer));
}

Multiple criteria can be combined to reduce the result size, where several createCriteria() methods are defined within the session interface to create a Criteria object. Depending on the chosen domain model, a query for objects of type Customer may also include objects of type BusinessCustomer and PrivateCustomer (see Figure 1).

Another example for criteria is the definition of a string or a number value for an attribute. In addition, constraints, such as “greater than” or “like,” can be applied by using Hibernate restrictions. A restriction is added to a criterion as visualized in the following listing:

Listing 3. Query by criteria with constraint

 Criteria crit = session.createCriteria(Customer.class);
crit.add(Restrictions.gt("customerSince", date));
Criteria crit2 = crit.createCriteria("address");
crit2.addOrder(Order.asc("zip"));
crit.addOrder(Order.asc("id"));
List result = crit.list();

The sequence of the result list can be ordered as ascending or descending by adding an Order attribute to a Criterion. Multiple Orders can be combined as shown in the example: First, the result list is ordered as ascending by ZIP codes. Then, within the same ZIP codes, the objects are ordered by ID. The two criteria are combined to use associations as an order parameter. In the example, the associated criterion Address is added to the criterion Customer.class to use the attributes of Address for sorting. The constraint “greater than”—Restrictions.gt()—is applied to reduce the number of returned clients with a date value. Further constraints like le() (less than or equal to) or like() and ilike() (case-insensitive “like”) exist.

The criteria query technique features flexible ways of conducting queries, such as using reflection for criteria creation, where the queries executed during runtime do not need to be specified before compilation or deployment. Instead of building criteria objects programmatically, existing object instances can be used for querying as illustrated below.

Query by example

An existing object can be used as a pattern to search for similar objects by using example queries. Attributes with a non-null value are used as a comparison parameter by default, but it is possible to ignore existing properties by calling the method excludeProperty() with the appropriate property.

A query by example is demonstrated in the following code, which queries for contracts assigned by customers attended by a certain key account manager. In other words: “The turnovers must be analyzed—please show me all contracts of customers assigned to key account manager Ralph Morris.”

Listing 4. Query by example

 KeyAccountMgr kam = new KeyAccountMgr();
kam.setName("Ralph Morris");
Criteria crit = session.createCriteria(Contract.class);
Criteria crit2 = crit.createAlias("customers", "customer");
Criteria crit3 = crit2.createCriteria("customer.keyAccountMgr");
crit3.add(Example.create(kam));
List result = crit3.list();

The code snippet visualizes the use of an example query; however, it does not demonstrate a reasonable use of a query by example, because the object is hard coded immediately before the query. In a real-life scenario, an example query would use an already existing object, such as one created by a user or another application, as demonstrated in the case below.

Query by example applied

To demonstrate an effective use of an example query, a walkthrough from a Web form to the database and back to the graphical user interface is illustrated here. To concentrate on the key points, the form is kept simple, with form validation omitted. A search criterion can be chosen via a select box, and a case-insensitive compare string can be inserted in a text input field. After submitting the form, a bean of type KeyAccountManager is instantiated. The search string is associated with the appropriate attribute of the bean (see Listings 5 and 7). The object, which has been created in this scenario by a Web request, is a good example for the use of an example query. The next step was introduced in the section above: the object is used for a query by example, which returns a list of corresponding persistent objects from the database. The result list then displays on the user interface. Figure 2 illustrates the described process.

The mapping of the HTML select box options and the object attribute is completed in the faces-config.xml file. This XML file is the central configuration file for Webpages based on JavaServer Faces (JSF):

Listing 5. Query by example applied

 <faces-config>
   <converter>
<description>Converts "" to null</description>
      <converter-id>emptyStringToNull</converter-id>
<converter-class>com.bachlmayr.hibcrit.jsf.
EmptyStringToNullConverter</converter-class>
   </converter>
   <!-- Managed Beans -->
   <managed-bean>
      <description>Address</description>
      <managed-bean-name>Address</managed-bean-name>
<managed-bean-class>com.bachlmayr.hibcrit.
pojo.Address</managed-bean-class>
   <managed-bean-scope>request</managed-bean-scope>
   ...
</faces-config>

The JSF mapping enables the reference of business objects in the HTML code. The reference is called by the managed-bean-name and the attributes of the business object, visualized in Listing 7. When empty HTML input fields are posted, according to the HTTP standard, they appear as empty strings ("") in the business logic. But when leaving an attribute empty in a query, we usually want to ignore this attribute, instead of searching for a name such as "". JSF offers a variety of converters (see the JavaServer Faces specification) to translate an HTML input to a Java type. Custom converters can be created by implementing the Converter interface. In this article’s example, empty strings are converted to null. Therefore, those empty arguments are ignored when building an example query. The custom converter implementation can be seen in the code example below:

Listing 6. JSF custom converter class

 public class EmptyStringToNullConverter implements Converter{
   
   /** Convert from presentation view to model view */
   public Object getAsObject(FacesContext ctx, 
      UIComponent comp, String value) {
         return (!value.equals(""))? new String(value) : null;
      }
   
   /** Convert other way round */
   public String getAsString(FacesContext ctx, 
      UIComponent comp, Object value) {
      ... ... // More code here
   }
}

The custom converter is referenced within the input text field tag to replace a potential empty string posted by the HTML form. The attribute name of the KeyAccountMgr object is treated as a null value in the business logic if the field remains empty:

Listing 7. Converted JSF used in HTML code

 <!— corresponding HTML code -->
<h:inputText value="#{KeyAccountMgr.name}" id="name" converter="emptyStringToNull"/>
<!— end of HTML snippet -->

Conclusion

Most Web applications need to persist data from data-entry forms or the like. The main issues in existing persistence strategies include the flexibility of queries, initial development effort, and maintainability. Hibernate addresses these challenges through the use of programmatic queries. The possibility of using reflection and having the compiler check the code improves time to market and flexibility, and the centralized mapping helps minimize maintenance effort. Accordingly, Hibernate can offer real advantages compared to using plain JDBC or Enterprise JavaBeans for achieving persistence. On the downside, while Hibernate is mostly compatible with the upcoming EJB 3 standard, it still does not implement it. Therefore, switching to another persistence runtime causes more effort when compared to the effort of changing runtimes that implement the same standard, provided the specification does not allow discrepancies of deployment descriptors or configuration files.

As a software consultant focusing on J2EE architecture, Gerald Bachlmayr is presently working on a development project in the finance industry, which will be rolled out for a number of banks in Germany shortly. In his eight years in software design and development, he has served in several positions, including development team lead, software architect, and quality assurance team lead. From a technical perspective, he focuses on J2EE technologies such as Hibernate, EJB, Struts, application servers, database modeling, and XML. He is a Sun-certified developer for business and Web components and for the Java 2 Platform. In the past, he has worked for industries such as banking, online brokerages, aircraft construction, aviation, telecommunication, and publishing.