Simple enumerations, like BookCategoryEL
and
PublicationFormEL
, are coded 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 coded 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) @NotNull( message="An original language is required!") @Enumerated( EnumType.STRING) private LanguageEL originalLanguage; @Column( nullable=false) @NotNull( message="A category is required!") @Enumerated( EnumType.STRING) private BookCategoryEL category; @Convert( converter = pl.m.converter.OtherAvailableLanguagesConverter.class) private Set<LanguageEL> otherAvailableLanguages; @Column( nullable=false) @Size( min=1, message="At least one publication form is required!") @Convert( converter=pl.m.converter.PublicationFormsConverter.class) 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 storage
serialization. It takes one of the following two parameter values:
EnumType.STRING
means that enumeration literals
are converted to strings when they are serialized ( e.g., in the
case of the originalLanguage
attribute of the
Book
class, the values saved in the database are one
of "EN", "DE", "FR" or "ES") ;
EnumType.ORDINAL
means that enumeration
literals are converted to their index integer when they are
serialized (i.e.,the values saved in the database are 1, 2 ,3
etc.).
We store the collection values of a multi-valued
enumeration attribute (like otherAvailableLanguages
and
publicationForms
) in a database table column as serialized
arrays in the form of ["value1", "value2", ...]
. In the
case of the originalLanguage
attribute, an example of a
saved value would 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
annotation allows specifying a Java converter that
is responsible for the mappings.
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.
For example, in the case of publicationForms
, we use
@Size( min=1, message = "At least one publication form is
required!")
, which enforces to have at least one value for this
attribute, otherwise the error message is displayed.
@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 create(...) throws Exception {...}
public static void update(...) throws Exception {...}
public static void delete(...) throws Exception {...}
public String getOtherAvailableLanguagesValues() {...}
public SelectItem[] getLanguageItems() {...}
public SelectItem[] getCategoryItems() {...}
public String getPublicationFormsValues() {...}
public SelectItem[] getPublicationFormsItems() {...}
}
Notice the new methods in the last code block, like
getPublicationFormsValues
and getPublicationFormsItems
, which are
used for handling the enumeration attributes in the UI. We discuss each of them in the following
subsections.
A JPA attribute converter is a special Java class that implements the
AttributeConverter
interface with the methods convertToDatabaseColumn
and convertToEntityAttribute
. For the 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
. In our example, the
convertToDatabaseColumn
method is responsible to convert
the entity attribute value (e.g., otherAvailableLanguages
)
to a JSON array which is stored in the database as a
String
.
The convertToEntityAttribute
method is responsible for de-serializing a
table column's value to the corresponding entity attribute value. In our example, this means to
map the JSON array string to a Java Set
of enumeration literals of type
LanguageEL
.
The code above shows the custom attribute converter class for the
otherAvailableLanguages
attribute. The attribute converter
class for the publicationForms
attribute is defined in the
same way.
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;
}
As explained in Chapter 4, the database schema can be automatically generated by a Java EE
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 objects 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, ..., RollbackException {
Book book = null;
// first clear existing data
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);
}