In this example we implement the Book hierarchy shown in Figure 18.1. The Java data model is derived from this design model.
We make the Java data model in 3 steps:
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.
Turn the plaform-independent datatypes (defined as the ranges of attributes) into Java datatypes.
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:
Compared to the validation app discussed in Part 2 of this tutorial, we have to deal with a number of new issues:
In the model code we have to take care of:
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.
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
.
In the UI code we have to take care of:
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.
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".
The Java data model can be directly encoded for getting the code of the model classes of our Java backend app.
Encode the enumeration type (to be used in the JSF views) .
Encode the model classes and add the corresponding JPA annotations for class hierarchy.
These steps are discussed in more detail in the following sections.
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; } }
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.
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, )
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.
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
Adding a segment information column ("Special type") to the
display table of the "List all books" use case in
WebContent/views/books/listAll.xhtml
.
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.
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.
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.