3. Write the Model Code

How to Encode a JPA Entity Class Model

3.1. Encode the enumerations

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;
  }
}

3.2. Encode the entity class

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.

3.3. Define a converter for serializing enumeration attribute values

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).

3.4. Encode the enumeration attribute setters

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.

3.5. Write a serialization function

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).

3.6. Database schema

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.

3.7. Creating test data

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);	
}