2. Case Study 1: Eliminating a Class Hierarchy with Single Table Inheritance

In this example we implement the Book hierarchy shown in Figure 18.1. The Java data model is derived from this design model.

2.1. Make the Java data model

We make the Java data model in 3 steps:

  1. For every class in the model, we create a Java class with the corresponding properties and add the set and get methods corresponding to each property.

  2. Turn the plaform-independent datatypes (defined as the ranges of attributes) into Java datatypes.

  3. Add the set and get methods corresponding to each direct property of every class in the hierarchy (subclasses must not define set and get method for properties in superclasses, but only for their direct properties).

This leads to the Java data model shown in Figure 18.4:

Figure 18.4. The Java data model


2.2. New issues

Compared to the validation app discussed in Part 2 of this tutorial, we have to deal with a number of new issues:

  1. In the model code we have to take care of:

    1. Encoding the enumeration type (BookTypeEL in our example) used in the views rendering to create a select list for the special case of book. Notice that this enumeration is not really used in the model classes (we discuss further the reasons), but only with the purpose to have the book category (i.e., special type) rendered in the JSF views. Read Part 3 enumeration app for detailed instructions on how to implement enumerations in Java.

    2. Encode each class from the Book hierarchy and use the JPA specific annotations required to persist the JavaBeans in a single database table, i.e., books.

  2. In the UI code we have to take care of:

    1. Adding a "Special type" column to the display table of the "List all books" use case in WebContent/views/books/listAll.xhtml. A book without a special category will have an empty table cell, while for all other books their category will be shown in this cell, along with other segment-specific attribute values.

    2. Adding a "Special type" select control, and corresponding form fields for all segment properties, in the forms of the "Create book" and "Update book" use cases in WebContent/views/books/create.xhtml and WebContent/views/books/update.xhtml. Segment property form fields are only displayed, and their validation is performed, when a corresponding book category has been selected. Such an approach of rendering specific form fields only on certain conditions is sometimes called "dynamic forms".

2.3. Encode the model classes of the Java data model

The Java data model can be directly encoded for getting the code of the model classes of our Java backend app.

2.3.1. Summary

  1. Encode the enumeration type (to be used in the JSF views) .

  2. Encode the model classes and add the corresponding JPA annotations for class hierarchy.

These steps are discussed in more detail in the following sections.

2.3.2. Encode the enumeration type BookTypeEL

The enumeration type BookTypeEL is encoded as a Java enum, and we probide enumeration literals as well as assign label (human readable) to each enumeration literal:

public enum BookTypeEL {
  TEXTBOOK( "TextBook"), BIOGRAPHY( "Biography");

  // associated label for an enumeration literal
  private final String label;

  private BookTypeEL( String label) {
    this.label = label;
  }

  public String getLabel() {
    return this.label;
  }
}

2.3.3. Encode the model classes for the Book hierarchy

We encode the model class Book in the form of a Java class and we use the appropriate JPA annotations to be used with the Book class hierarchy:

@Entity
@Inheritance( strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn( name = "category", discriminatorType = DiscriminatorType.STRING, length = 16)
@DiscriminatorValue( value = "BOOK")
@Table( name = "books")
...
public class Book {
  @Id
  @Column( length = 10)
  @NotNull( message = "An ISBN is required!")
  @Pattern( regexp = "\\b\\d{9}(\\d|X)\\b", message = "The ISBN must be a 10-digit string or a 9-digit string followed by 'X'!")
  private String isbn;
  @Column( nullable = false)
  @NotNull( message = "A title is required!")
  private String title;
  @Column( nullable = false)
  @NotNull( message = "A year is required!")
  @UpToNextYear
  @Min( value = 1459, message = "The year must not be before 1459!")
  private Integer year;

  // constructors, set, get and other methods
}

When using the JPA single table storage for a class hierarchy, the @Inheritance annotations must be set for the superclass (e.g, Book in our example). It provide the strategy parameter which for single table storage must have the value InheritanceType.SINGLE_TABLE. The @DiscriminatorColumn specifies the table column name, i.e., name = "category", which stores the value that makes the difference between different types of subclasses (e.g., Book, TextBook and Biography in our example). We expect to use BookTypeEL for the values of name parameter, but this is not possible because the name parameter requires a constant expression, while BookCategoryEL.TEXTBOOK.name() is not considered to be a constant (being a function cally), so a compiler exception is thrown. Last, the @DiscriminatorValue specifies an unique value to be stored in the discriminator table column used to identify the class type (e.g., for the Book we want to have BOOK as discriminator value, so we use value = "BOOK").

Further, we define the TextBook and Biography subtypes:

@Entity
@DiscriminatorValue( value = "TEXTBOOK")
...
public class TextBook extends Book {
  @NotNull( message = "A subject area value is required")
  String subjectArea;
  
  // constructors, set, get and other methods
}

The TextBook and Biography are subclasses of Book, so we define them by using extends Book. For each of the subclass, we add the @DiscriminatorValue JPA annotation with an unique value to identify the type.

2.4. Database schema for single table class hierarchy

As a result of the @Inheritance( strategy = InheritanceType.SINGLE_TABLE) annotation, only one database table is used for each class hierarchy. This table contains the columns corresponding to all the properties of all the classes from the hierarchy plus the discriminator column. In our example, the books table contains the following columns: isbn, title, year, subjectArea, about and category. The simplified corresponding SQL-DDL code internally used to generate the books table for our application is shown below:

CREATE TABLE IF NOT EXISTS `books` (
  `ISBN` varchar(10) NOT NULL,
  `TITLE` varchar(255) NOT NULL,
  `YEAR` int(11) NOT NULL,
  `SUBJECTAREA` varchar(255) DEFAULT NULL,
  `ABOUT` varchar(255) DEFAULT NULL,
  `category` varchar(16) DEFAULT NULL,
) 

2.5. Write the View and Controller Code

The user interface (UI) consists of a start page that allows navigating to the data management pages (in our example, to WebContent/views/books/index.xhtml). Such a data management page contains 4 sections: list books, create book, update book and delete book.

2.5.1. Summary

We have to take care of handling the category discriminator and the subjectArea and about segment properties both in the "List all books" use case as well as in the "Create book" and "Update book" use cases by

  1. Adding a segment information column ("Special type") to the display table of the "List all books" use case in WebContent/views/books/listAll.xhtml.

  2. Adding a "Special type" select control, and corresponding form fields for all segment properties, in the forms of the "Create book" and "Update book" use cases in WebContent/views/books/create.xhtml and WebContent/views/books/create.xhtml. Segment property form fields are only displayed, and their validation occurs only when a corresponding book category has been selected.

2.5.2. Adding a segment information column in "List all books"

We add a "Special type" column to the display table of the "List all books" use case in books.html:

<h:dataTable value="#{bookController.books}" var="b">
  ...
  <h:column>
    <f:facet name="header">Special type</f:facet>
    #{b.getClass().getSimpleName() != 'Book' ? b.getClass().getSimpleName() : ''}
  </h:column>
</h:dataTable>

A conditional expression (using the Java ? ternary operator) is used to check if the JavaBean class name is Book, on which case we don't show it, or it is something else (e.g. TextBook or Biography), and then it is shown. This expression also shows how you can call/use various methods from the JavaBeans, not only the methods custom defined by the programmer of the application.

2.5.3. Adding a "Special type" select control in "Create book" and "Update book"

In both use cases, we need to allow selecting a special category of book ('textbook' or 'biography') with the help of a select control, as shown in the following HTML fragment:

<h:panelGrid id="bookPanel" columns="3">
  ...
  <h:outputText value="Special type: " />
  <h:selectOneMenu id="bookType" value="#{viewScope.bookTypeVal}">
    <f:selectItem itemLabel="---"/>
    <f:selectItems value="#{book.typeItems}" />
    <f:ajax event="change" execute="@this" 
            render="textBookPanel biographyBookPanel standardBookPanel" />
  </h:selectOneMenu>
  <h:message for="bookType" errorClass="error" />
</h:panelGrid>

<h:panelGroup id="standardBookPanel">  
  <h:commandButton value="Create" rendered="#{viewScope.bookTypeVal == null}"
                   action="#{bookController.add( book.isbn, book.title, book.year)}" />
</h:panelGroup>
    
<h:panelGroup id="textBookPanel">  
  <h:panelGrid rendered="#{viewScope.bookTypeVal == 'TEXTBOOK'}" columns="3">
    <h:outputText value="Subject area: " />
    <h:inputText id="subjectArea" value="#{textBook.subjectArea}"/>
    <h:message for="subjectArea" errorClass="error" />
  </h:panelGrid>
  <h:commandButton value="Create" rendered="#{viewScope.bookTypeVal == 'TEXTBOOK'}"
                   action="#{textBookController.add( book.isbn, book.title, book.year, textBook.subjectArea)}" />
</h:panelGroup>

<h:panelGroup id="biographyBookPanel">  
  <h:panelGrid rendered="#{viewScope.bookTypeVal == 'BIOGRAPHY'}" columns="3">
    <h:outputText value="About: " />
    <h:inputText id="about" value="#{biographyBook.about}"/>
    <h:message for="about" errorClass="error" />
  </h:panelGrid>
  <h:commandButton value="Create" rendered="#{viewScope.bookTypeVal == 'BIOGRAPHY'}"
                   action="#{biographyBookController.add( book.isbn, book.title, book.year, biographyBook.about)}" />
</h:panelGroup>

There are a few important remarks on the above view code:

  • the h:selectOneMenu is used to create a single selection list which is populated with book tiutles by using the getTypeItems method of the Book class. A more detailed explanation is presented in Part 3 enumeration app.

  • it is possible to conditionally render JSF view components by using the rendered attribute. The JSF EL expression must return true or false, this making the HTML resulting elements to be part of the HTML DOM or not. Notice that the conditional expressions are evaluated in the server side. This is the method we use to hide or show the input form elements corresponding to various book types (e.g., TextBook has a subjectArea property while Biography has an about property).

  • the render attribute used with f:ajax specifies which of the JSF components are to be updated . This is needed because of the live DOM changes (client side, not server side) which applies after the AJAX call.

  • AJAX is used to submit form for reevaluation when the special type select list is changed (something is selected). As a result, it enforces the rendering of the three panels corresponding to three book cases: simple book, text book and biography. Using execute="@this" we enforce the re-evaluation of the form at server side, so the resulting HTML DOM structure contains the changes accordiong with the conditions specified by the rendered attributes of the various JSF elements. Notice that an JSF element which has a conditional rendered expression must be child of another JSF element which is always part of the DOM.

  • h:panelGroup is used to define a set of elements which are shown or hidden.

  • the action attribute of the h:commandButton can't be used with conditional expressions, therefore we have to create three command buttons, one for each case: create/update a Book, a TextBook or a Biography. The add method of the corresponding controller class (i.e., BookController, TextBookController or BiographyController is called).

  • since we do not have a corresponding property in the JavaBean class(es) for the special type (category), we can use JSF variables to store the value of the single select list, and then use the variable in rendered conditions for various elements. Therefore, for the h:selectOneMenu, the value="#{viewScope.bookTypeVal}" specifies that we use a "on the fly" defined property named bookTypeVal, which is part of the view scope internal JSF object(s). We can also define such variable outside the view scope, e.g., value="#{bookTypeVal}", but in this case they are request scoped, so their value is lost after submitting the form, and the rendered conditions can't be correctly evaluated.

For a class in the class hierarchy, one corresponding controller class is defined. It contains the specific add, update and destroy methods (see . The shared methods, such as getAllObjects are defined by the controller of the top level class (e.g., for our example, this is BookController). See minimal app for more details on how to implement the controller class and the corresponding CRUD methods.

The Update Book test case is very similar with the Create Book test case. The Delete Book test case remains unchanged, see See minimal app for more details.