Simple enumerations, like BookCategoryEL
and PublicationFormEL
,
are encoded in the following way with the help of Java's enum
construct:
public enum BookCategoryEL { NOVEL, BIOGRAPHY, TEXTBOOK, OTHER; }
Notice how the enumeration literals are defined with capitalized names in a comma-separated list.
For a code list, like LanguageEL
, where each enumeration instance consists
of a code and a label, we use the codes as enumeration literals and define an attribute
label
for storing the labels, as well as a private constructor that allows
creating enumeration instances consisting of a code and a label. This is possible because in
Java, an enum is a special kind of class. Each entry in the list of enumeration literals, auch
as EN( "English")
), represents an invocation of the enum's constructor:
public enum LanguageEL {
EN( "English"),
DE( "German"),
FR( "French"),
ES( "Spanish");
private final String label;
private LanguageEL( String label) {
this.label = label;
}
public String getLabel() {
return this.label;
}
}
The entity class Book
is encoded with the help of JPA annotations for class
attributes, as well as the setter and getter methods of every class attribute.
@Entity @Table( name="books") @ViewScoped @ManagedBean( name="book") public class Book { @Id @Column( length=10) @NotNull( message="An ISBN value is required!") @Pattern( regexp="\\b\\d{9}(\\d|X)\\b", message = "...") private String isbn; @Column( nullable=false) @NotNull( message="A title is required!") private String title; @Column( nullable=false) @Enumerated( EnumType.STRING) @NotNull( message="An original language is required!") private LanguageEL originalLanguage; @Column( nullable=false) @Enumerated( EnumType.STRING) @NotNull( message="A category is required!") private BookCategoryEL category; @Convert( converter = pl.model.converter.OtherAvailableLanguagesConverter.class) private Set<LanguageEL> otherAvailableLanguages; @Column( nullable=false) @Convert( converter = pl.model.converter.PublicationFormsConverter.class) @Size( min=1, message="At least one publication forms is required!") private Set<PublicationFormEL> publicationForms; ... }
For the case of single-valued enumeration attributes (like originalLanguage
and category
), the JPA annotation @Enumerated
is used for specifying
the type of value used when enumeration literals are serialized for being stored in a database
table column. It accepts as parameter one of the following two values:
EnumType.STRING
- the enumeration literal converted to string is used when
the attribute value needs to be stored in the database ( e.g., for the
originalLanguage
attribute of the Book
class, the values saved in
the database are one of EN
, DE
, FR
or ES
)
;
EnumType.ORDINAL
- the corresponding ordinal value of the enumeration literal is
used when the attribute value needs to be stored in the database (i.e., the positive integers
correpsonding to the enumeration literal).
We store the collection values of a multi-valued enumeration attribute (like
otherAvailableLanguages
and publicationForms
) in a database table
column as serialized arrays, i.e., in the form of [ "value1", "value2", ...]
, which
for example in the case of originalLanguage
attribute, a possible saved value can
be: ["EN", "FR", "DE"]
. Achieving this behavior is possible with JPA by using
custom converters to map database table column values to Java types and vice versa. The
@Convert
attribute annotation specifies a Java converter that is responsible for
the mappings with the converter
parameter.
Also, in the case of a multi-valued enumeration attribute, we use the @Size
annotation to specify the minimum, and if required also the maximum, number of elements stored
by this attribute. This is used as one of the validation which applies to the attribute, and
create the corresponding error messages. For example, in the case of
publicationForms
, we use @Size( min=1, message = "At least one publication
forms is required!")
, which enforces to have at least one value for this attribute,
otherwise the defined custom error message is used.
@Entity @Table( name="books")
@ViewScoped @ManagedBean( name="book")
public class Book {
...
public Book( String isbn, String title, Integer year,
LanguageEL originalLanguage,
Set<LanguageEL> otherAvailableLanguages,
BookCategoryEL category,
Set<PublicationFormEL> publicationForms) { ...}
public LanguageEL getOriginalLanguage() {...}
public void setOriginalLanguage( LanguageEL originalLanguage) {...}
public Set<LanguageEL> getOtherAvailableLanguages() {...}
public void setOtherAvailableLanguages(
Set<LanguageEL> otherAvailableLanguages) {...}
public BookCategoryEL getCategory() {...}
public void setCategory( BookCategoryEL category) {...}
public Set<PublicationFormEL> getPublicationForms() {...}
public void setPublicationForms(
Set<PublicationFormEL> publicationForms) {...}
public static Book retrieve(...) {...}
public static List<Book> retrieveAll(...) {...}
public static void add(...) throws Exception {...}
public static void update(...) throws Exception {...}
public static void destroy(...) throws Exception {...}
public String getOtherAvailableLanguagesValues() {...}
public SelectItem[] getLanguageItems() {...}
public SelectItem[] getCategoryItems() {...}
public String getPublicationFormsValues() {...}
public SelectItem[] getPublicationFormsItems() {...}
}
The Book
constructor is extended with the new parameters corresponding to
the new enumeration attributes. Notice the new annotations, used for the single and
multivalued enumeration attributes, as well as the additional methods (e.g.,
getPublicationFormsValues
and getPublicationFormsItems
) used
with the purpose to fill with data the UI elements, corresponding to the enumeration
attributes and to display multi-valued enumeration as string serializations on the view
tables. We discuss each of these additional parts in the following sections.
A JPA attribute converter is a special Java class that implements the
AttributeConverter
interface with the methods convertToDatabaseColumn
and convertToEntityAttribute
. For the case of otherAvailableLanguages
attribute, we define the following converter
class:
@Converter public class OtherAvailableLanguagesConverter implements AttributeConverter<Set<LanguageEL>, String> { @Override public String convertToDatabaseColumn( Set<LanguageEL> attrValue) { JSONArray jsonArr = new JSONArray( attrValue); return jsonArr.toString(); } @Override public Set<LanguageEL> convertToEntityAttribute( String colValue) { Set<LanguageEL> result = new HashSet<LanguageEL>(); try { JSONArray jsonArr = new JSONArray( colValue); for ( int i = 0, length = jsonArr.length(); i < length; i++) { result.add( LanguageEL.valueOf( jsonArr.getString( i))); } } catch ( JSONException e) { e.printStackTrace(); } return result; } }
An attribute converter class needs to be annotated with @Converter
. Java
generics are used to specify the types used for the data mappings, e.g.,
Set<LanguageEL>
for the JavaBean code and String
for the database
column.
In our case, the required convertToDatabaseColumn
method is responsible to
serialize the JavaBean attribute value (e.g., otherAvailableLanguages
) to a JSON
array which is stored in the database as a String
serialization. The corresponding
method body uses the Java built-in JSON capabilities obtain the final serialization of the JSON
array as String
.
The convertToEntityAttribute
method is responsible to obtain the JavaBean
attribute value from the corresponding table column stored value. In our case, this means to
map the JSON array serialized as string to a Java Set
of
LanguageEL
literal values. In the same way, as for the case of
convertToDatabaseColumn
, the Java built-in JSON capabilities are used
together with the Java enumeration specific methods to perform the de-serialization.
The above code shows how to create the custom attribute converter class for the
otherAvailableLanguages
attribute, but in exactly the same way is defined
also the attribute converter class for publicationForms
attribute. The only
differences are in the used Java generic types (PublicationFormEL
in place of
LanguageEL
) and the parameter types of convertToDatabaseColumn
,
i.e., public String convertToDatabaseColumn( Set<PublicationFormEL>
attrValue)
.
Both for single-valued and for multi-valued enumeration attributes an ordinary setter is
defined. In the case of a multi-valued enumeration attribute, this setter assigns an entire
set of values (in the form of a Java Set
) to the attribute.
The object serialization function now needs to include the values of enumeration attributes:
public String toString() {
int i = 0, n = this.publicationForms.size();
String result = "{ isbn: '" + this.isbn + "', title:'" + this.title
+ "', year: " + this.year + ", originalLanguage: '" + this.originalLanguage.getLabel();
result += "', otherAvailableLanguages: [";
for ( LanguageEL oal : this.otherAvailableLanguages) {
result += "'" + oal.getLabel() + "'";
if ( i < n - 1) {
result += ", ";
}
i++;
}
result += "]";
result += ", category: '" + this.category.name().toLowerCase();
result += "', publicationForms: [";
i = 0;
for ( PublicationFormEL pf : this.publicationForms) {
result += "'" + pf.name().toLowerCase() + "'";
if ( i < n - 1) {
result += ", ";
}
i++;
}
result += "]}";
return result;
}
Notice that for multi-valued enumeration attributes we can't use the
toString()
function because this will give the default Set
serialization which is not what we like to have (is not really human readable).
As we discussed in Part 1, the database schema can be automatically generated by a
JPA-enabled server like TomEE. The generated schema for our Book
entity class is
like so:
CREATE TABLE IF NOT EXISTS `books` ( `ISBN` varchar(10) NOT NULL, `CATEGORY` varchar(255) NOT NULL, `ORIGINALLANGUAGE` varchar(255) NOT NULL, `OTHERAVAILABLELANGUAGES` varchar(255) DEFAULT NULL, `PUBLICATIONFORMS` varchar(255) NOT NULL, `TITLE` varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
For every attribute (like category
), a column with the same name but using
upper case is created, (e.g., CATEGORY
). This is the default naming, which is fine
for our example application, but it can be changed, if required, by using the @Column(
name="...")
annotation, as in @Column( name="book_category")
.
The single-valued and multi-valued enumeration attributes are by default created as
varchar
columns with the default maximum length of 255
characters.
However, if a longer (or shorter) length is desirable, this can be enforced by using the
@Column( length=...)
annotation.
In the test data records that are created by Book.createTestData
, we now have
to provide values for single- and multi-valued enumeration
attributes:
public static void createTestData( EntityManager em, UserTransaction ut)
throws NotSupportedException, SystemException, IllegalStateException,
SecurityException, HeuristicMixedException, HeuristicRollbackException,
RollbackException {
Book book = null;
// first clear existing books
Book.clearData( em, ut);
ut.begin();
Book book = new Book( "006251587X", "Weaving the Web", 2000,
LanguageEL.EN,
new HashSet<LanguageEL>() {
{ add( LanguageEL.DE); add( LanguageEL.FR);}
},
BookCategoryEL.TEXTBOOK,
new HashSet<PublicationFormEL>() {
{ add( PublicationFormEL.HARDCOVER);
add( PublicationFormEL.PDF);}
}
);
em.persist( book);
}