Chapter 5. Mapping value types

In this chapter

  • Mapping basic properties
  • Mapping embeddable components
  • Controlling mapping between Java and SQL types

After spending the previous chapter almost exclusively on entities and the respective class- and identity-mapping options, we now focus on value types in their various forms. We split value types into two categories: basic value-typed classes that come with the JDK, such as String, Date, primitives, and their wrappers; and developer-defined value-typed classes, such as Address and MonetaryAmount in CaveatEmptor.

In this chapter, we first map persistent properties with JDK types and learn the basic mapping annotations. You see how to work with various aspects of properties: overriding defaults, customizing access, and generated values. You also see how SQL is used with derived properties and transformed column values. We wrap up basic properties with temporal properties and mapping enumerations. We then discuss custom value-typed classes and map them as embeddable components. You learn how classes relate to the database schema and make your classes embeddable, while allowing for overriding embedded attributes. We complete embeddable components by mapping nested components. Finally, we discuss how to customize loading and storing of property values at a lower level with flexible JPA converters, a standardized extension point of every JPA provider.

Major new features in JPA 2

  • Switchable access through either field or property getter/setter methods for an entity hierarchy, or individual properties, with the @Access annotation
  • Nesting multiple levels of embeddable component classes, and the ability to apply @AttributeOverride to nested embedded properties with dot notation
  • Addition of Converter API for basic-typed attributes, so you can control how values are loaded and stored and transform them if necessary

5.1. Mapping basic properties

When you map a persistent class, whether it’s an entity or an embeddable type (more about these later, in section 5.2), all of its properties are considered persistent by default. The default JPA rules for properties of persistent classes are these:

  • If the property is a primitive or a primitive wrapper, or of type String, BigInteger, BigDecimal, java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp, byte[], Byte[], char[], or Character[], it’s automatically persistent. Hibernate loads and stores the value of the property in a column with an appropriate SQL type and the same name as the property.
  • Otherwise, if you annotate the class of the property as @Embeddable, or you map the property itself as @Embedded, Hibernate maps the property as an embedded component of the owning class. We discuss embedding of components later in this chapter, with the Address and MonetaryAmount embeddable classes of CaveatEmptor.
  • Otherwise, if the type of the property is java.io.Serializable, its value is stored in its serialized form. This typically isn’t what you want, and you should always map Java classes instead of storing a heap of bytes in the database. Imagine maintaining a database with this binary information when the application is gone in a few years.
  • Otherwise, Hibernate will throw an exception on startup, complaining that it doesn’t understand the type of the property.

This configuration by exception approach means you don’t have to annotate a property to make it persistent; you only have to configure the mapping in an exceptional case. Several annotations are available in JPA to customize and control basic property mappings.

5.1.1. Overriding basic property defaults

You might not want all properties of an entity class to be persistent. For example, although it makes sense to have a persistent Item#initialPrice property, an Item#totalPriceIncludingTax property shouldn’t be persistent if you only compute and use its value at runtime, and hence shouldn’t be stored in the database. To exclude a property, mark the field or the getter method of the property with the @javax.persistence.Transient annotation or use the Java transient keyword. The transient keyword usually only excludes fields for Java serialization, but it’s also recognized by JPA providers.

We’ll come back to the placement of the annotation on fields or getter methods in a moment. Let’s assume as we have before that Hibernate will access fields directly because @Id has been placed on a field. Therefore, all other JPA and Hibernate mapping annotations are also on fields.

If you don’t want to rely on property mapping defaults, apply the @Basic annotation to a particular property—for example, the initialPrice of an Item:

@Basic(optional = false)
BigDecimal initialPrice;

We have to admit that this annotation isn’t very useful. It only has two parameters: the one shown here, optional, marks the property as not optional at the Java object level. By default, all persistent properties are nullable and optional; an Item may have an unknown initialPrice. Mapping the initialPrice property as non-optional makes sense if you have a NOT NULL constraint on the INITIALPRICE column in your SQL schema. If Hibernate is generating the SQL schema, it will include a NOT NULL constraint automatically for non-optional properties.

Now, when you store an Item and forget to set a value on the initialPrice field, Hibernate will complain with an exception before hitting the database with an SQL statement. Hibernate knows that a value is required to perform an INSERT or UPDATE. If you don’t mark the property as optional and try to save a NULL, the database will reject the SQL statement, and Hibernate will throw a constraint-violation exception. There isn’t much difference in the end result, but it’s cleaner to avoid hitting the database with a statement that fails. We’ll talk about the other parameter of @Basic, the fetch option, when we explore optimization strategies later, in section 12.1.

Instead of @Basic, most engineers use the more versatile @Column annotation to declare nullability:

@Column(nullable = false)
BigDecimal initialPrice;

We’ve now shown you three ways to declare whether a property value is required: with the @Basic annotation, the @Column annotation, and earlier with the Bean Validation @NotNull annotation in section 3.3.2. All have the same effect on the JPA provider: Hibernate does a null check when saving and generates a NOT NULL constraint in the database schema. We recommend the Bean Validation @NotNull annotation so you can manually validate an Item instance and/or have your user interface code in the presentation layer execute validation checks automatically.

The @Column annotation can also override the mapping of the property name to the database column:

@Column(name = "START_PRICE", nullable = false)
BigDecimal initialPrice;

The @Column annotation has a few other parameters, most of which control SQL-level details such as catalog and schema names. They’re rarely needed, and we only show them throughout this book when necessary.

Property annotations aren’t always on fields, and you may not want Hibernate to access fields directly.

5.1.2. Customizing property access

The persistence engine accesses the properties of a class either directly through fields or indirectly through getter and setter methods. An annotated entity inherits the default from the position of the mandatory @Id annotation. For example, if you’ve declared @Id on a field, not a getter method, all other mapping annotations for that entity are expected on fields. Annotations are never on the setter methods.

The default access strategy isn’t only applicable to a single entity class. Any @Embedded class inherits the default or explicitly declared access strategy of its owning root entity class. We cover embedded components later in this chapter. Furthermore, Hibernate accesses any @MappedSuperclass properties with the default or explicitly declared access strategy of the mapped entity class. Inheritance is the topic of chapter 6.

The JPA specification offers the @Access annotation for overriding the default behavior, with the parameters AccessType.FIELD and AccessType.PROPERTY. If you set @Access on the class/entity level, Hibernate accesses all properties of the class according to the selected strategy. You then set any other mapping annotations, including the @Id, on either fields or getter methods, respectively.

You can also use the @Access annotation to override the access strategy of individual properties. Let’s explore this with an example.

Listing 5.1. Overriding access strategy for the name property

Path: /model/src/main/java/org/jpwh/model/advanced/Item.java

  1. The Item entity defaults to field access. The @Id is on a field. (You also move the brittle ID_GENERATOR string into a constant.)
  2. The @Access(AccessType.PROPERTY) setting on the name field switches this particular property to runtime access through getter/setter methods by the JPA provider.
  3. Hibernate calls getName() and setName() when loading and storing items.

Note that the position of other mapping annotations like @Column doesn’t change—only how instances are accessed at runtime.

Now turn it around: if the default (or explicit) access type of the entity would be through property getter and setter methods, @Access(AccessType.FIELD) on a getter method would tell Hibernate to access the field directly. All other mapping information would still have to be on the getter method, not the field.

Hibernate Feature

Hibernate has a rarely needed extension: the noop property accessor. This sounds strange, but it lets you refer to a virtual property in queries. This is useful if you have a database column you’d like to use only in JPA queries. For example, let’s say the ITEM database table has a VALIDATED column and your Hibernate application won’t access this column through the domain model. It might be a legacy column or a column maintained by another application or database trigger. All you want is to refer to this column in a JPA query such as select i from Item i where i.validated = true or select i.id, i.validated from Item i. The Java Item class in your domain model doesn’t have this property; hence there is no place to put annotations. The only way to map such a virtual property is with an hbm.xml native metadata file:

<hibernate-mapping>
    <class name="Item">
        <id name="id">
           ...

        </id>
        <property name="validated"
                  column="VALIDATED"
                  access="noop"/>
    </class>
</hibernate-mapping>

This mapping tells Hibernate that you’d like to access the virtual Item#validated property, mapped to the VALIDATED column, in queries; but for value read/writes at runtime, you want “no operation” on an instance of Item. The class doesn’t have that attribute. Remember that such a native mapping file has to be complete: any annotations on the Item class are now ignored!

If none of the built-in access strategies are appropriate, you can define your own customized property-access strategy by implementing the interface org.hibernate.property.PropertyAccessor. You enable a custom accessor by setting its fully qualified name in a Hibernate extension annotation: @org.hibernate.annotations .AttributeAccessor("my.custom.Accessor"). Note that AttributeAccessor is new in Hibernate 4.3 and replaces the deprecated org.hibernate.annotations.AccessType, which was easily confused with the JPA enum javax.persistence.AccessType.

Some properties don’t map to a column. In particular, a derived property takes its value from an SQL expression.

5.1.3. Using derived properties

Hibernate Feature

The value of a derived property is calculated at runtime by evaluating an SQL expression declared with the @org.hibernate.annotations.Formula annotation; see the next listing.

Listing 5.2. Two read-only derived properties
@org.hibernate.annotations.Formula(
    "substr(DESCRIPTION, 1, 12) || '...'"
)
protected String shortDescription;
<enter/>
@org.hibernate.annotations.Formula(
    "(select avg(b.AMOUNT) from BID b where b.ITEM_ID = ID)"
)
protected BigDecimal averageBidAmount;

The given SQL formulas are evaluated every time the Item entity is retrieved from the database and not at any other time, so the result may be outdated if other properties are modified. The properties never appear in an SQL INSERT or UPDATE, only in SELECTs. Evaluation occurs in the database; Hibernate embeds the SQL formula in the SELECT clause when loading the instance.

Formulas may refer to columns of the database table, they can call SQL functions, and they may even include SQL subselects. In the previous example, the SUBSTR() function is called, as well as the || concat operator. The SQL expression is passed to the underlying database as is; if you aren’t careful, you may rely on vendor-specific operators or keywords and bind your mapping metadata to a particular database product. Notice that unqualified column names refer to columns of the table of the class to which the derived property belongs.

The database evaluates SQL expressions in formulas only when Hibernate retrieves an entity instance from the database. Hibernate also supports a variation of formulas called column transformers, allowing you to write a custom SQL expression for reading and writing a property value.

5.1.4. Transforming column values

Hibernate Feature

Let’s say you have a database column called IMPERIALWEIGHT, storing the weight of an Item in pounds. The application, however, has the property Item#metricWeight in kilograms, so you have to convert the value of the database column when reading and writing a row from and to the ITEM table. You can implement this with a Hibernate extension: the @org.hibernate.annotations.ColumnTransformer annotation.

Listing 5.3. Transforming column values with SQL expressions
@Column(name = "IMPERIALWEIGHT")
@org.hibernate.annotations.ColumnTransformer(
    read = "IMPERIALWEIGHT / 2.20462",
    write = "? * 2.20462"
)
protected double metricWeight;

When reading a row from the ITEM table, Hibernate embeds the expression IMPERIALWEIGHT / 2.20462, so the calculation occurs in the database and Hibernate returns the metric value in the result to the application layer. For writing to the column, Hibernate sets the metric value on the mandatory, single placeholder (the question mark), and your SQL expression calculates the actual value to be inserted or updated.

Hibernate also applies column converters in query restrictions. For example, the following query retrieves all items with a weight of two kilograms:

List<Item> result =
    em.createQuery("select i from Item i where i.metricWeight = :w")
        .setParameter("w", 2.0)
        .getResultList();

The actual SQL executed by Hibernate for this query contains the following restriction in the WHERE clause:

// ...
where
    i.IMPERIALWEIGHT / 2.20462=?

Note that your database probably won’t be able to rely on an index for this restriction; you’ll see a full table scan, because the weight for all ITEM rows has to be calculated to evaluate the restriction.

Another special kind of property relies on database-generated values.

5.1.5. Generated and default property values

Hibernate Feature

The database sometimes generates a property value, usually when you insert a row for the first time. Examples of database-generated values are a creation timestamp, a default price for an item, and a trigger that runs for every modification.

Typically, Hibernate applications need to refresh instances that contain any properties for which the database generates values, after saving. This means you would have to make another round trip to the database to read the value after inserting or updating a row. Marking properties as generated, however, lets the application delegate this responsibility to Hibernate. Essentially, whenever Hibernate issues an SQL INSERT or UPDATE for an entity that has declared generated properties, it does a SELECT immediately afterward to retrieve the generated values.

You mark generated properties with the @org.hibernate.annotations.Generated annotation.

Listing 5.4. Database-generated property values
@Temporal(TemporalType.TIMESTAMP)
@Column(insertable = false, updatable = false)
@org.hibernate.annotations.Generated(
    org.hibernate.annotations.GenerationTime.ALWAYS
)
protected Date lastModified;
<enter/>
@Column(insertable = false)
@org.hibernate.annotations.ColumnDefault("1.00")
@org.hibernate.annotations.Generated(
    org.hibernate.annotations.GenerationTime.INSERT
)
protected BigDecimal initialPrice;

Available settings for GenerationTime are ALWAYS and INSERT.

With ALWAYS, Hibernate refreshes the entity instance after every SQL UPDATE or INSERT. The example assumes that a database trigger will keep the lastModified property current. The property should also be marked read-only, with the updatable and insertable parameters of @Column. If both are set to false, the property’s column(s) never appear in the INSERT or UPDATE statements, and you let the database generate the value.

With GenerationTime.INSERT, refreshing only occurs after an SQL INSERT, to retrieve the default value provided by the database. Hibernate also maps the property as not insertable. The @ColumnDefault Hibernate annotation sets the default value of the column when Hibernate exports and generates the SQL schema DDL.

Timestamps are frequently automatically generated values, either by the database, as in the previous example, or by the application. Let’s have a closer look at the @Temporal annotation you saw in listing 5.4.

5.1.6. Temporal properties

The lastModified property of the last example was of type java.util.Date, and a database trigger on SQL INSERT generated its value. The JPA specification requires that you annotate temporal properties with @Temporal to declare the accuracy of the SQL data type of the mapped column. The Java temporal types are java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, and java.sql.Timestamp. Hibernate also supports the classes of the java.time package available in JDK 8. (Actually, the annotation isn’t required if a converter is applied or applicable for the property. You’ll see converters again later in this chapter.)

The next listing shows a JPA-compliant example: a typical “this item was created on” timestamp property that is saved once but never updated.

Listing 5.5. Property of a temporal type that must be annotated with @Temporal

Available TemporalType options are DATE, TIME, and TIMESTAMP, establishing what part of the temporal value should be stored in the database.

Hibernate Feature

Hibernate defaults to TemporalType.TIMESTAMP when no @Temporal annotation is present. Furthermore, you’ve used the @CreationTimestamp Hibernate annotation to mark the property. This is a sibling of the @Generated annotation from the previous section: it tells Hibernate to generate the property value automatically. In this case, Hibernate sets the value to the current time before it inserts the entity instance into the database. A similar built-in annotation is @UpdateTimestamp. You can also write and configure custom value generators, running in the application or database. Have a look at org.hibernate.annotations.GeneratorType and ValueGenerationType.

Another special property type is enumerations.

5.1.7. Mapping enumerations

An enumeration type is a common Java idiom where a class has a constant (small) number of immutable instances. In CaveatEmptor, for example, you can apply this to auctions:

public enum AuctionType {
    HIGHEST_BID,
    LOWEST_BID,
    FIXED_PRICE
}

You can now set the appropriate auctionType on each Item:

Without the @Enumerated annotation, Hibernate would store the ORDINAL position of the value. That is, it would store 1 for HIGHEST_BID, 2 for LOWEST_BID, and 3 for FIXED_PRICE. This is a brittle default; if you make changes to the AuctionType enum, existing values may no longer map to the same position. The EnumType.STRING option is therefore a better choice; Hibernate stores the label of the enum value as is.

This completes our tour of basic properties and their mapping options. So far, we have been showing properties of JDK-supplied types such as String, Date, and BigDecimal. Your domain model also has custom value-typed classes, those with a composition association in the UML diagram.

5.2. Mapping embeddable components

So far, the classes of the domain model you’ve mapped have all been entity classes, each with its own life cycle and identity. The User class, however, has a special kind of association with the Address class, as shown in figure 5.1.

Figure 5.1. Composition of User and Address

In object-modeling terms, this association is a kind of aggregation—a part-of relationship. Aggregation is a strong form of association; it has some additional semantics with regard to the life cycle of objects. In this case, you have an even stronger form, composition, where the life cycle of the part is fully dependent on the life cycle of the whole. A composed class in UML such as Address is often a candidate value type for your object/relational mapping.

5.2.1. The database schema

Let’s map such a composition relationship with Address as a value type, with the same semantics as String or BigDecimal, and User as an entity. First, have a look at the SQL schema you’re targeting, in figure 5.2.

Figure 5.2. The columns of the components are embedded in the entity table.

There is only one mapped table, USERS, for the User entity. This table embeds all details of the components, where a single row holds a particular User and their homeAddress and billingAddress. If another entity has a reference to an Address—for example, Shipment#deliveryAddress—then the SHIPMENT table will also have all columns needed to store an Address.

This schema reflects value type semantics: a particular Address can’t be shared; it doesn’t have its own identity. Its primary key is the mapped database identifier of the owning entity. An embedded component has a dependent life cycle: when the owning entity instance is saved, the component instance is saved. When the owning entity instance is deleted, the component instance is deleted. Hibernate doesn’t even have to execute any special SQL for this; all the data is in a single row.

Having “more classes than tables” is how Hibernate supports fine-grained domain models. Let’s write the classes and mapping for this structure.

5.2.2. Making classes embeddable

Java has no concept of composition—a class or property can’t be marked as a component or composition life cycle. The only difference from an entity is the database identifier: a component class has no individual identity; hence, the component class requires no identifier property or identifier mapping. It’s a simple POJO, as you can see in the following listing.

Listing 5.6. Address class: an embeddable component

Path: /model/src/main/java/org/jpwh/model/simple/Address.java

  1. Instead of @Entity, this component POJO is marked with @Embeddable. It has no identifier property.
  2. Hibernate calls this no-argument constructor to create an instance and then populates the fields directly.
  3. You can have additional (public) constructors for convenience.

The properties of the embeddable class are all by default persistent, just like the properties of a persistent entity class. You can configure the property mappings with the same annotations, such as @Column or @Basic. The properties of the Address class map to the columns STREET, ZIPCODE, and CITY and are constrained with NOT NULL.

Issue: Hibernate Validator doesn’t generate NOT NULL constraints

At the time of writing, an open issue remains with Hibernate Validator: Hibernate won’t map @NotNull constraints on embeddable component properties to NOT NULL constraints when generating your database schema. Hibernate will only use @Not-Null on your components’ properties at runtime, for Bean Validation. You have to map the property with @Column(nullable = false) to generate the constraint in the schema. The Hibernate bug database is tracking this issue as HVAL-3.

That’s the entire mapping. There’s nothing special about the User entity:

Path: /model/src/main/java/org/jpwh/model/simple/User.java

Hibernate detects that the Address class is annotated with @Embeddable; the STREET, ZIPCODE, and CITY columns are mapped on the USERS table, the owning entity’s table.

When we talked about property access earlier in this chapter, we mentioned that embeddable components inherit their access strategy from their owning entity. This means Hibernate will access the properties of the Address class with the same strategy as for User properties. This inheritance also affects the placement of mapping annotations in embeddable component classes. The rules are as follows:

  • If the owning @Entity of an embedded component is mapped with field access, either implicitly with @Id on a field or explicitly with @Access(AccessType.FIELD) on the class, all mapping annotations of the embedded component class are expected on fields of the component class. Hibernate expects annotations on the fields of the Address class and reads/writes the fields directly at runtime. Getter and setter methods on Address are optional.
  • If the owning @Entity of an embedded component is mapped with property access, either implicitly with @Id on a getter method or explicitly with @Access(AccessType.PROPERTY) on the class, all mapping annotations of the embedded component class are expected on getter methods of the component class. Hibernate then reads and writes values by calling getter and setter methods on the embeddable component class.
  • If the embedded property of the owning entity class—User#homeAddress in the last example—is marked with @Access(AccessType.FIELD), Hibernate expects annotations on the fields of the Address class and access fields at runtime.
  • If the embedded property of the owning entity class—User#homeAddress in the last example—is marked with @Access(AccessType.PROPERTY), Hibernate expects annotations on getter methods of the Address class and access getter and setter methods at runtime.
  • If @Access annotates the embeddable class itself, Hibernate will use the selected strategy for reading mapping annotations on the embeddable class and runtime access.

Hibernate Feature

There’s one more caveat to remember: there’s no elegant way to represent a null reference to an Address. Consider what would happen if the columns STREET, ZIPCODE, and CITY were nullable. When Hibernate loads a User without any address information, what should be returned by someUser.getHomeAddress()? Hibernate returns a null in this case. Hibernate also stores a null embedded property as NULL values in all mapped columns of the component. Consequently, if you store a User with an “empty” Address (you have an Address instance but all its properties are null), no Address instance will be returned when you load the User. This can be counterintuitive; on the other hand, you probably shouldn’t have nullable columns anyway and avoid ternary logic.

You should override the equals() and hashCode() methods of Address and compare instances by value. This isn’t critically important as long as you don’t have to compare instances: for example, by putting them in a HashSet. We’ll discuss this issue later, in the context of collections; see section 7.2.1.

In a more realistic scenario, a user would probably have separate addresses for different purposes. Figure 5.1 showed an additional composition relationship between User and Address: the billingAddress.

5.2.3. Overriding embedded attributes

The billingAddress is another embedded component property of the User class, so another Address has to be stored in the USERS table. This creates a mapping conflict: so far, you only have columns in the schema to store one Address in STREET, ZIPCODE, and CITY.

You need additional columns to store another Address for each USERS row. When you map the billingAddress, override the column names:

Path: /model/src/main/java/org/jpwh/model/simple/User.java

The @Embedded annotation actually isn’t necessary. It’s an alternative to @Embeddable: mark either the component class or the property in the owning entity class (both doesn’t hurt but has no advantage). The @Embedded annotation is useful if you want to map a third-party component class without source and no annotations, but using the right getter/setter methods (like regular JavaBeans).

The @AttributeOverrides selectively overrides property mappings of the embedded class; in this example, you override all three properties and provide different column names. Now you can store two Address instances in the USERS table, each instance in a different set of columns (check the schema again in figure 5.2).

Each @AttributeOverride for a component property is “complete”: any JPA or Hibernate annotation on the overridden property is ignored. This means the @Column annotations on the Address class are ignored—all BILLING_* columns are NULLable! (Bean Validation still recognizes the @NotNull annotation on the component property, though; Hibernate only overrides persistence annotations.)

You can further improve reusability of your domain model, and make it more fine-grained, by nesting embedded components.

5.2.4. Mapping nested embedded components

Let’s consider the Address class and how it encapsulates address details: instead of a simple city string, you could move this detail into a new City embeddable class. Look at the changed domain model diagram in figure 5.3. The SQL schema we’re targeting for the mapping still has only one USERS table, as shown in figure 5.4.

Figure 5.3. Nested composition of Address and City

Figure 5.4. Embedded columns hold Address and City details.

An embeddable class can have an embedded property. Address has a city property:

Path: /model/src/main/java/org/jpwh/model/advanced/Address.java

@Embeddable
public class Address {
<enter/>
    @NotNull
    @Column(nullable = false)
    protected String street;
<enter/>
    @NotNull
    @AttributeOverrides(
        @AttributeOverride(
            name = "name",
            column = @Column(name = "CITY", nullable = false)
        )
    )
    protected City city;
<enter/>
    // ...
}

The embeddable City class has only basic properties:

Path: /model/src/main/java/org/jpwh/model/advanced/City.java

You could continue this kind of nesting by creating a Country class, for example. All embedded properties, no matter how deep they are in the composition, are mapped to columns of the owning entity’s table—here, the USERS table.

You can declare @AttributeOverrides at any level, as you do for the name property of the City class, mapping it to the CITY column. This can be achieved with either (as shown) an @AttributeOverride in Address or an override in the root entity class, User. Nested properties can be referenced with dot notation: for example, on User#address, @AttributeOveride(name = "city.name") references the Address #City#name attribute.

We’ll come back to embedded components later, in section 7.2. You can even map collections of components or have references from a component to an entity.

At the beginning of this chapter, we talked about basic properties and how Hibernate maps a JDK type such as java.lang.String, for example, to an appropriate SQL type. Let’s find out more about this type system and how values are converted at a lower level.

5.3. Mapping Java and SQL types with converters

Until now, you’ve assumed that Hibernate selects the right SQL type when you map a java.lang.String property. Nevertheless, what is the correct mapping between the Java and SQL types, and how can you control it?

5.3.1. Built-in types

Any JPA provider has to support a minimum set of Java-to-SQL type conversions; you saw this list at the beginning of this chapter, in section 5.1. Hibernate supports all of these mappings, as well as some additional adapters that aren’t standard but are useful in practice. First, the Java primitives and their SQL equivalents.

Primitive and numeric types

The built-in types shown in table 5.1 map Java primitives, and their wrappers, to appropriate SQL standard types. We’ve also included some other numeric types.

Table 5.1. Java primitive types that map to SQL standard types

Name

Java type

ANSI SQL type

integer int, java.lang.Integer INTEGER
long long, java.lang.Long BIGINT
short short, java.lang.Short SMALLINT
float float, java.lang.Float FLOAT
double double, java.lang.Double DOUBLE
byte byte, java.lang.Byte TINYINT
boolean boolean, java.lang.Boolean BOOLEAN
big_decimal java.math.BigDecimal NUMERIC
big_integer java.math.BigInteger NUMERIC

The names are Hibernate-specific; you’ll need them later when customizing type mappings.

You probably noticed that your DBMS product doesn’t support some of the mentioned SQL types. These SQL type names are ANSI-standard type names. Most DBMS vendors ignore this part of the SQL standard, usually because their legacy type systems predate the standard. But JDBC provides a partial abstraction of vendor-specific data types, allowing Hibernate to work with ANSI-standard types when executing DML statements such as INSERT and UPDATE. For product-specific schema generation, Hibernate translates from the ANSI-standard type to an appropriate vendor-specific type using the configured SQL dialect. This means you usually don’t have to worry about SQL data types if you let Hibernate create the schema for you.

If you have an existing schema and/or you need to know the native data type for your DBMS, look at the source of your configured SQL dialect. For example, the H2Dialect shipping with Hibernate contains this mapping from the ANSI NUMERIC type to the vendor-specific DECIMAL type: registerColumnType(Types.NUMERIC, "decimal($p,$s)").

The NUMERIC SQL type supports decimal precision and scale settings. The default precision and scale setting, for a BigDecimal property, for example, is NUMERIC(19, 2). To override this for schema generation, apply the @Column annotation on the property and set its precision and scale parameters.

Next are types that map to strings in the database.

Character types

Table 5.2 shows types that map character and string value representations.

Table 5.2. Adapters for character and string values

Name

Java type

ANSI SQL type

string java.lang.String VARCHAR
character char[], Character[], java.lang.String CHAR
yes_no boolean, java.lang.Boolean CHAR(1), 'Y' or 'N'
true_false boolean, java.lang.Boolean CHAR(1), 'T' or 'F'
class java.lang.Class VARCHAR
locale java.util.Locale VARCHAR
timezone java.util.TimeZone VARCHAR
currency java.util.Currency VARCHAR

The Hibernate type system picks an SQL data type depending on the declared length of a string value: if your String property is annotated with @Column(length = ...) or @Length of Bean Validation, Hibernate selects the right SQL data type for the given string size. This selection also depends on the configured SQL dialect. For example, for MySQL, a length of up to 65,535 produces a regular VARCHAR(length) column when the schema is generated by Hibernate. For a length of up to 16,777,215, a MySQL-specific MEDIUMTEXT data type is produced, and even greater lengths use a LONGTEXT. The default length of Hibernate for all java.lang.String properties is 255, so without any further mapping, a String property maps to a VARCHAR(255) column. You can customize this type selection by extending the class of your SQL dialect; read the dialect documentation and source code to find out more details for your DBMS product.

A database usually enables internationalization of text with a sensible (UTF-8) default character set for your entire database, or at least whole tables. This is a DBMS-specific setting. If you need more fine-grained control and want to switch to NVARCHAR, NCHAR, or NCLOB column types, annotate your property mapping with @org.hibernate.annotations.Nationalized.

Also built in are some special converters for legacy databases or DBMSs with limited type systems, such as Oracle. The Oracle DBMS doesn’t even have a truth-valued data type, the only data type required by the relational model. Many existing Oracle schemas therefore represent Boolean values with Y/N or T/F characters. Or—and this is the default in Hibernate’s Oracle dialect—a column of type NUMBER(1,0) is expected and generated. Again, we refer you to the SQL dialect of your DBMS if you want to know all mappings from ANSI data type to vendor-specific type.

Next are types that map to dates and times in the database.

Date and time types

Table 5.3 lists types associated with dates, times, and timestamps.

Table 5.3. Date and time types

Name

Java type

ANSI SQL type

date java.util.Date, java.sql.Date DATE
time java.util.Date, java.sql.Time TIME
timestamp java.util.Date, java.sql.Timestamp TIMESTAMP
calendar java.util.Calendar TIMESTAMP
calendar_date java.util.Calendar DATE
duration java.time.Duration BIGINT
instant java.time.Instant TIMESTAMP
localdatetime java.time.LocalDateTime TIMESTAMP
localdate java.time.LocalDate DATE
localtime java.time.LocalTime TIME
offsetdatetime java.time.OffsetDateTime TIMESTAMP
offsettime java.time.OffsetTime TIME
zoneddatetime java.time.ZonedDateTime TIMESTAMP

In your domain model, you may choose to represent date and time data as either java.util.Date, java.util.Calendar, or the subclasses of java.util.Date defined in the java.sql package. This is a matter of taste, and we leave the decision to you—make sure you’re consistent. You might not want to bind the domain model to types from the JDBC package.

You can also use the Java 8 API in the java.time package. Note that this is Hibernate-specific and not standardized in JPA 2.1.

Hibernate’s behavior for java.util.Date properties might surprise you at first: when you store a java.util.Date, Hibernate won’t return a java.util.Date after loading. It will return a java.sql.Date, a java.sql.Time, or a java.sql.Timestamp, depending on whether you mapped the property with TemporalType.DATE, TemporalType.TIME, or TemporalType.TIMESTAMP.

Hibernate has to use the JDBC subclass when loading data from the database because the database types have higher accuracy than java.util.Date. A java.util.Date has millisecond accuracy, but a java.sql.Timestamp includes nanosecond information that may be present in the database. Hibernate won’t cut off this information to fit the value into java.util.Date. This Hibernate behavior may lead to problems if you try to compare java.util.Date values with the equals() method; it’s not symmetric with the java.sql.Timestamp subclass’s equals() method.

The solution is simple, and not even specific to Hibernate: don’t call aDate.equals(bDate). You should always compare dates and times by comparing Unix time milliseconds (assuming you don’t care about the nanoseconds): aDate.getTime() > bDate.getTime(), for example, is true if aDate is a later time than bDate. Be careful: collections such as HashSet call the equals() method as well. Don’t mix java.util.Date and java.sql.Date|Time|Timestamp values in such a collection. You won’t have this kind of problem with a Calendar property. If you store a Calendar value, Hibernate will always return a Calendar value, created with Calendar.getInstance() (the actual type depends on locale and time zone).

Alternatively, you can write your own converter, as shown later in this chapter, and transform any instance of a java.sql temporal type, given to you by Hibernate, into a plain java.util.Date instance. A custom converter is also a good starting point if, for example, a Calendar instance should have a non-default time zone after loading the value from the database.

Next are types that map to binary data and large values in the database.

Binary and large value types

Table 5.4 lists types for handling binary data and large values. Note that only binary is supported as the type of an identifier property.

First, consider how Hibernate represents your potentially large value, as binary or text.

Table 5.4. Binary and large value types

Name

Java type

ANSI SQL type

binary byte[], java.lang.Byte[] VARBINARY
text java.lang.String CLOB
clob java.sql.Clob CLOB
blob java.sql.Blob BLOB
serializable java.io.Serializable VARBINARY

If a property in your persistent Java class is of type byte[], Hibernate maps it to a VARBINARY column. The real SQL data type depends on the dialect; for example, in PostgreSQL, the data type is BYTEA, and in Oracle DBMS, it’s RAW. In some dialects, the length set with @Column also has an effect on the selected native type: for example, LONG RAW for length of 2000 and greater in Oracle.

A java.lang.String property is mapped to an SQL VARCHAR column, and the same for char[] and Character[]. As we’ve discussed, some dialects register different native types depending on declared length.

In both cases, Hibernate initializes the property value right away, when the entity instance that holds the property variable is loaded. This is inconvenient when you have to deal with potentially large values, so you usually want to override this default mapping. The JPA specification has a convenient shortcut annotation for this purpose, @Lob:

@Entity
public class Item {
<enter/>
    @Lob
    protected byte[] image;
<enter/>
    @Lob
    protected String description;
<enter/>
    // ...
}

This maps the byte[] to an SQL BLOB data type and the String to a CLOB. Unfortunately, you still don’t get lazy loading with this design. Hibernate would have to intercept field access and, for example, load the bytes of the image when you call someItem.getImage(). This approach requires bytecode instrumentation of your classes after compilation, for the injection of extra code. We’ll discuss lazy loading through bytecode instrumentation and interception in section 12.1.3.

Alternatively, you can switch the type of property in your Java class. JDBC supports locator objects (LOBs) directly. If your Java property is java.sql.Clob or java.sql.Blob, you get lazy loading without bytecode instrumentation:

@Entity
public class Item {
<enter/>
    @Lob
    protected java.sql.Blob imageBlob;

    @Lob
    protected java.sql.Clob description;
<enter/>
    // ...
What does BLOB/CLOB mean?

Jim Starkey, who came up with the idea of LOBs, says that the marketing department created the terms BLOB and CLOB and that they don’t mean anything. You can interpret them any way you like. We prefer locator objects, as a hint that they’re placeholders that help us locate and access the real thing.

These JDBC classes include behavior to load values on demand. When the owning entity instance is loaded, the property value is a placeholder, and the real value isn’t immediately materialized. Once you access the property, within the same transaction, the value is materialized or even streamed directly (to the client) without consuming temporary memory:

Path: /examples/src/test/java/org/jpwh/test/advanced/LazyProperties.java

The downside is that your domain model is then bound to JDBC; in unit tests, you can’t access LOB properties without a database connection.

Hibernate Feature

To create and set a Blob or Clob value, Hibernate offers some convenience methods. This example reads byteLength bytes from an InputStream directly into the database, without consuming temporary memory:

Finally, Hibernate provides fallback serialization for any property type that is java.io.Serializable. This mapping converts the value of the property to a byte stream stored in a VARBINARY column. Serialization and deserialization occur when the owning entity instance is stored and loaded. Naturally, you should use this strategy with extreme caution, because data lives longer than applications. One day, nobody will know what those bytes in your database mean. Serialization is sometimes useful for temporary data, such as user preferences, login session data, and so on.

Hibernate will pick the right type adapter depending on the Java type of your property. If you don’t like the default mapping, read on to override it.

Selecting a type adapter
Hibernate Feature

You have seen many adapters and their Hibernate names in the previous sections. Use the name when you override Hibernate’s default type selection, and explicitly select a particular adapter:

@Entity
public class Item {
<enter/>
    @org.hibernate.annotations.Type(type = "yes_no")
    protected boolean verified = false;
}

Instead of BIT, this boolean now maps to a CHAR column with values Y or N.

You can also override an adapter globally in the Hibernate boot configuration with a custom user type, which you’ll learn how to write later in this chapter:

metaBuilder.applyBasicType(new MyUserType(), new String[]{"date"});

This setting will override the built-in date type adapter and delegate value conversion for java.util.Date properties to your custom implementation.

We consider this extensible type system one of Hibernate’s core features and an important aspect that makes it so flexible. Next, we explore the type system and JPA custom converters in more detail.

5.3.2. Creating custom JPA converters

A new requirement for the online auction system is multiple currencies. Rolling out this kind of change can be complex. You have to modify the database schema, you may have to migrate existing data from the old to the new schema, and you have to update all applications that access the database. In this section, we show you how JPA converters and the extensible Hibernate type system can assist you in this process, providing an additional, flexible buffer between your application and the database.

To support multiple currencies, let’s introduce a new class in the CaveatEmptor domain model: MonetaryAmount, shown in the following listing.

Listing 5.7. Immutable MonetaryAmount value-type class

Path: /model/src/main/java/org/jpwh/model/advanced/MonetaryAmount.java

  1. This value-typed class should be java.io.Serializable: when Hibernate stores entity instance data in the shared second-level cache (see section 20.2), it disassembles the entity’s state. If an entity has a MonetaryAmount property, the serialized representation of the property value is stored in the second-level cache region. When entity data is retrieved from the cache region, the property value is deserialized and reassembled.
  2. The class doesn’t need a special constructor. You can make it immutable, even with final fields, because your code will be the only place an instance is created.
  3. You should implement the equals() and hashCode() methods and compare monetary amounts “by value.”
  4. You need a String representation of a monetary amount. Implement the toString() method and a static method to create an instance from a String.

Next, you update other parts of the auction domain model and use MonetaryAmount for all properties involving money, such as Item#buyNowPrice and Bid#amount.

Converting basic property values

As is often the case, the database folks can’t implement multiple currencies right away and need more time. All they can provide quickly is a column data type change, in the database schema. They suggest that you store the BUYNOWPRICE in the ITEM table in a VARCHAR column and that you append the currency code of the monetary amount to its string value. You store, for example, the value 11.23 USD or 99 EUR.

You have to convert an instance of MonetaryAmount to such a String representation when storing data. When loading data, you convert the String back into a MonetaryAmount.

The simplest solution is a javax.persistence.AttributeConverter, as shown in the next listing, a standardized extension point in JPA.

Listing 5.8. Converting between strings and MonetaryValue

Path: /model/src/main/java/org/jpwh/converter/MonetaryAmountConverter.java

A converter has to implement the AttributeConverter interface; the two type arguments are the type of the Java property and the type in the database schema. The Java type is MonetaryAmount, and the database type is String, which maps, as usual, to an SQL VARCHAR. You must annotate the class with @Converter or declare it as such in the orm.xml metadata. With autoApply enabled, any MonetaryAmount property in your domain model, be it of an entity or an embeddable class, without further mapping will now be handled by this converter automatically. (Don’t be distracted by the convertToEntityAttribute() method of the AttributeConverter interface; it’s not the best name.)

An example of such a MonetaryAmount property in the domain model is Item#buyNowPrice:

Path: /model/src/main/java/org/jpwh/model/advanced/converter/Item.java

The @Convert annotation is optional: apply it to override or disable a converter for a particular property. @Column renames the mapped database column PRICE; the default is BUYNOWPRICE. For automatic schema generation, define it as VARCHAR with a length of 63 characters.

Later, when your DBA upgrades the database schema and offers you separate columns for the monetary amount value and currency, you only have to change your application in a few places. Drop the MonetaryAmountConverter from your project and make MonetaryAmount an @Embeddable; it then maps automatically to two database columns. It’s easy to selectively enable and disable converters, too, if some tables in the schema haven’t been upgraded.

The converter you just wrote is for MonetaryAmount, a new class in the domain model. Converters aren’t limited to custom classes: you can even override Hibernate’s built-in type adapters. For example, you could create a custom converter for some or even all java.util.Date properties in your domain model.

You can apply converters to properties of entity classes, like Item#buyNowPrice in the last example. You can also apply them to properties of embeddable classes.

Converting properties of components

We’ve been making the case for fine-grained domain models in this chapter. Earlier, you isolated the address information of the User and mapped the Address embeddable class. Let’s continue this process and introduce inheritance, with an abstract Zipcode class as shown in figure 5.5.

Figure 5.5. The abstract Zipcode class has two concrete subclasses.

The Zipcode class is trivial, but don’t forget to implement equality by value:

Path: /model/src/main/java/org/jpwh/model/advanced/converter/Zipcode.java

abstract public class Zipcode {
<enter/>
    protected String value;
<enter/>
    public Zipcode(String value) {
        this.value = value;
    }
<enter/>
    public String getValue() {
        return value;
    }
<enter/>
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Zipcode zipcode = (Zipcode) o;
        return value.equals(zipcode.value);
    }
<enter/>
    @Override
    public int hashCode() {
        return value.hashCode();
    }
}

You can now encapsulate domain subclasses, the difference between German and Swiss postal codes, and any processing:

Path: /model/src/main/java/org/jpwh/model/advanced/converter/GermanZipcode.java

public class GermanZipcode extends Zipcode {
<enter/>
    public GermanZipcode(String value) {
        super(value);
    }
}

You haven’t implemented any special processing in the subclass. Let’s start with the most obvious difference: German zip codes are five numbers long, Swiss are four. A custom converter will take care of this:

Path: /model/src/main/java/org/jpwh/converter/ZipcodeConverter.java

Hibernate calls the convertToDatabaseColumn() method of this converter when storing a property value; you return a String representation. The column in the schema is VARCHAR. When loading a value, you examine its length and create either a GermanZipcode or SwissZipcode instance. This is a custom type discrimination routine; you can pick the Java type of the given value.

Now apply this converter on some Zipcode properties—for example, the embedded homeAddress of a User:

Path: /model/src/main/java/org/jpwh/model/advanced/converter/User.java

The attributeName declares the zipcode attribute of the embeddable Address class. This setting supports a dot syntax for the attribute path; if zipcode isn’t a property of the Address class but is a property of a nested embeddable City class (as shown earlier in this chapter), reference it with city.zipcode, its nested path.

If several @Convert annotations are required on a single embedded property, to convert several attributes of the Address, for example, you can group them within an @Converts annotation. You can also apply converters to values of collections and maps, if their values and/or keys are of basic or embeddable type. For example, you can add the @Convert annotation on a persistent Set<Zipcode>. We’ll show you how to map persistent collections later, with @ElementCollection, in chapter 7.

For persistent maps, the attributeName option of the @Convert annotation has some special syntax:

  • On a persistent Map<Address, String>, you can apply a converter for the zipcode property of each map key with the attribute name key.zipcode.
  • On a persistent Map<String, Address>, you can apply a converter for the zipcode property of each map value with the attribute name value.zipcode.
  • On a persistent Map<Zipcode, String>, you can apply a converter for the key of each map entry with the attribute name key.
  • On a persistent Map<String, Zipcode>, you can apply a converter for the value of each map entry by not setting any attributeName.

As before, the attribute name can be a dot-separated path if your embeddable classes are nested; you can write key.city.zipcode to reference the zipcode property of the City class, in a composition with the Address class.

Some limitations of the JPA converters are as follows:

  • You can’t apply them to identifier or version properties of an entity.
  • You shouldn’t apply a converter on a property mapped with @Enumerated or @Temporal, because these annotations already declare what kind of conversion has to occur. If you want to apply a custom converter for enums or date/time properties, don’t annotate them with @Enumerated or @Temporal.
  • You can apply a converter to a property mapping in an hbm.xml file, but you have to prefix the name: type="converter:qualified.ConverterName".

Let’s get back to multiple currency support in CaveatEmptor. The database administrators changed the schema again and asked you to update the application.

5.3.3. Extending Hibernate with UserTypes

Hibernate Feature

Finally, you’ve added new columns to the database schema to support multiple currencies. The ITEM table now has a BUYNOWPRICE_AMOUNT and a separate column for the currency of the amount, BUYNOWPRICE_CURRENCY. There are also INITIALPRICE_ AMOUNT and INITIALPRICE_CURRENCY columns. You have to map these columns to the MonetaryAmount properties of the Item class, buyNowPrice and initialPrice.

Ideally, you don’t want to change the domain model; the properties already use the MonetaryAmount class. Unfortunately, the standardized JPA converters don’t support transformation of values from/to multiple columns. Another limitation of JPA converters is integration with the query engine. You can’t write the following query: select i from Item i where i.buyNowPrice.amount > 100. Thanks to the converter from the previous section, Hibernate knows how to convert a MonetaryAmount to and from a string. It doesn’t know that MonetaryAmount has an amount attribute, so it can’t parse such a query.

A simple solution would be to map MonetaryAmount as @Embeddable, as discussed earlier in this chapter for the Address class. Each property of MonetaryAmountamount and currency—maps to its respective database column.

Your database admins, however, add a twist to their requirements: because other, old applications also access the database, you have to convert each amount to a target currency before storing it in the database. For example, Item#buyNowPrice should be stored in US dollars, and Item#initialPrice should be stored in Euros. (If this example seems far-fetched, we can assure you that you’ll see worse in the real world. Evolution of a shared database schema can be costly but is of course necessary because data always lives longer than applications.) Hibernate offers a native converter API: an extension point that allows much more detailed and low-level customization access.

The extension points

Hibernate’s extension interfaces for its type system can found in the org.hibernate.usertype package. The following interfaces are available:

  • UserType—You can transform values by interacting with the plain JDBC PreparedStatement (when storing data) and ResultSet (when loading data). By implementing this interface, you can also control how Hibernate caches and dirty-checks values. The adapter for MonetaryAmount has to implement this interface.
  • CompositeUserType—This extends UserType, providing Hibernate with more details about your adapted class. You can tell Hibernate that the Monetary-Amount component has two properties: amount and currency. You can then reference these properties in queries with dot notation: for example, select avg(i.buyNowPrice.amount) from Item i.
  • ParameterizedUserType—This provides settings to your adapter in mappings. You have to implement this interface for the MonetaryAmount conversion, because in some mappings you want to convert the amount to US dollars and in other mappings to Euros. You only have to write a single adapter and can customize its behavior when mapping a property.
  • DynamicParameterizedType—This more powerful settings API gives you access to dynamic information in the adapter, such as the mapped column and table names. You might as well use this instead of ParameterizedUserType; there is no additional cost or complexity.
  • EnhancedUserType—This is an optional interface for adapters of identifier properties and discriminators. Unlike JPA converters, a UserType in Hibernate can be an adapter for any kind of entity property. Because MonetaryAmount won’t be the type of an identifier property or discriminator, you won’t need it.
  • UserVersionType—This is an optional interface for adapters of version properties.
  • UserCollectionType—This rarely needed interface is used to implement custom collections. You have to implement it to persist a non-JDK collection and preserve additional semantics.

The custom type adapter for MonetaryAmount will implement several of these interfaces.

Implementing the UserType

Because MonetaryAmountUserType is a large class, we examine it in several steps. These are the interfaces you implement:

Path: /model/src/main/java/org/jpwh/converter/MonetaryAmountUserType.java

public class MonetaryAmountUserType
    implements CompositeUserType, DynamicParameterizedType {
<enter/>
    // ...
}

First you implement DynamicParameterizedType. You need to configure the target currency for the conversion by examining a mapping parameter:

Path: /model/src/main/java/org/jpwh/converter/MonetaryAmountUserType.java

  1. You can access some dynamic parameters here, such as the name of the mapped columns, the mapped (entity) table, or even the annotations on the field/getter of the mapped property. You don’t need them in this example, though.
  2. You only use the convertTo parameter to determine the target currency when saving a value into the database. If the parameter hasn’t been set, default to US dollars. Next, here’s some scaffolding code that any UserType must implement:

Path: /model/src/main/java/org/jpwh/converter/MonetaryAmountUserType.java

  1. The method returnedClass adapts the given class, in this case MonetaryAmount.
  2. Hibernate can enable some optimizations if it knows that MonetaryAmount is immutable.
  3. If Hibernate has to make a copy of the value, it calls this method. For simple immutable classes like MonetaryAmount, you can return the given instance.
  4. Hibernate calls disassemble when it stores a value in the global shared second-level cache. You need to return a Serializable representation. For MonetaryAmount, a String representation is an easy solution. Or, because MonetaryAmount is Serializable, you could return it directly.
  5. Hibernate calls this method when it reads the serialized representation from the global shared second-level cache. You create a MonetaryAmount instance from the String representation. Or, if you stored a serialized MonetaryAmount, you could return it directly.
  6. This is called during EntityManager#merge() operations. You need to return a copy of the original. Or, if your value type is immutable, like MonetaryAmount, you can return the original.
  7. Hibernate uses value equality to determine whether the value was changed and the database needs to be updated. You rely on the equality routine you already wrote on the MonetaryAmount class.

The real work of the adapter happens when values are loaded and stored, as implemented with the following methods:

Path: /model/src/main/java/org/jpwh/converter/MonetaryAmountUserType.java

  1. This is called to read the ResultSet when a MonetaryAmount value has to be retrieved from the database. You take the amount and currency values as given in the query result and create a new instance of MonetaryAmount.
  2. This is called when a MonetaryAmount value has to be stored in the database. You convert the value to the target currency and then set the amount and currency on the provided PreparedStatement (unless MonetaryAmount was null, in which case you call setNull() to prepare the statement).
  3. Here you can implement whatever currency conversion routine you need. For the sake of the example, you double the value so you can easily test whether conversion was successful. You’ll have to replace this code with a real currency converter in a real application. It’s not a method of the Hibernate UserType API.

Finally, following are the methods required by the CompositeUserType interface, providing the details of the MonetaryAmount properties so Hibernate can integrate the class with the query engine:

Path: /model/src/main/java/org/jpwh/converter/MonetaryAmountUserType.java

public String[] getPropertyNames() {
    return new String[]{"value", "currency"};
}
<enter/>
public Type[] getPropertyTypes() {
    return new Type[]{
        StandardBasicTypes.BIG_DECIMAL,
        StandardBasicTypes.CURRENCY
    };
}
<enter/>
public Object getPropertyValue(Object component,
                               int property) {
    MonetaryAmount monetaryAmount = (MonetaryAmount) component;
    if (property == 0)
        return monetaryAmount.getValue();
    else
        return monetaryAmount.getCurrency();
}
<enter/>
public void setPropertyValue(Object component,
                             int property,
                             Object value) {
    throw new UnsupportedOperationException(
        "MonetaryAmount is immutable"
    );
}

The MonetaryAmountUserType is now complete, and you can already use it in mappings with its fully qualified class name in @org.hibernate.annotations.Type, as shown in the section “Selecting a type adapter.” This annotation also supports parameters, so you can set the convertTo argument to the target currency.

But we recommend that you create type definitions, bundling your adapter with some parameters.

Using type definitions

You need an adapter that converts to US dollars and an adapter that converts to Euros. If you declare these parameters once as a type definition, you don’t have to repeat them in property mappings. A good location for type definitions is package metadata, in a package-info.java file:

Path: /model/src/main/java/org/jpwh/converter/package-info.java

@org.hibernate.annotations.TypeDefs({
    @org.hibernate.annotations.TypeDef(
        name = "monetary_amount_usd",
        typeClass = MonetaryAmountUserType.class,
        parameters = {@Parameter(name = "convertTo", value = "USD")}
    ),
    @org.hibernate.annotations.TypeDef(
        name = "monetary_amount_eur",
        typeClass = MonetaryAmountUserType.class,
        parameters = {@Parameter(name = "convertTo", value = "EUR")}
    )
})
package org.jpwh.converter;
<enter/>
import org.hibernate.annotations.Parameter;

You’re now ready to use the adapters in mappings, using the names monetary_amount_usd and monetary_amount_eur.

Let’s map the buyNowPrice and initialPrice of Item:

Path: /model/src/main/java/org/jpwh/model/advanced/usertype/Item.java

@Entity
public class Item {
<enter/>
    @NotNull
    @org.hibernate.annotations.Type(
        type = "monetary_amount_usd"
    )
    @org.hibernate.annotations.Columns(columns = {
        @Column(name = "BUYNOWPRICE_AMOUNT"),
        @Column(name = "BUYNOWPRICE_CURRENCY", length = 3)
    })
    protected MonetaryAmount buyNowPrice;
<enter/>
    @NotNull
    @org.hibernate.annotations.Type(
        type = "monetary_amount_eur"
    )
    @org.hibernate.annotations.Columns(columns = {
        @Column(name = "INITIALPRICE_AMOUNT"),
        @Column(name = "INITIALPRICE_CURRENCY", length = 3)
    })
    protected MonetaryAmount initialPrice;
<enter/>
    // ...
}

If UserType transforms values for only a single column, you don’t need an @Column annotation. MonetaryAmountUserType, however, accesses two columns, so you need to explicitly declare two columns in the property mapping. Because JPA doesn’t support multiple @Column annotations on a single property, you have to group them with the proprietary @org.hibernate.annotations.Columns annotation. Note that order of the annotations is now important! Re-check the code for MonetaryAmountUserType; many operations rely on indexed access of arrays. The order when accessing PreparedStatement or ResultSet is the same as that of the declared columns in the mapping. Also note that the number of columns isn’t relevant for your choice of UserType versus CompositeUserType—only your desire to expose value type properties for queries.

With MonetaryAmountUserType, you’ve extended the buffer between the Java domain model and the SQL database schema. Both representations are now more robust to changes, and you can handle even rather eccentric requirements without modifying the essence of the domain model classes.

5.4. Summary

  • We discussed the mapping of basic and embedded properties of an entity class.
  • You saw how to override basic mappings, how to change the name of a mapped column, and how to use derived, default, temporal, and enumeration properties.
  • We covered embeddable component classes and how you can create fine-grained domain models.
  • You can map the properties of several Java classes in a composition, such as Address and City, to one entity table.
  • We looked at how Hibernate selects Java to SQL type converters, and what types are built into Hibernate.
  • You wrote a custom type converter for the MonetaryAmount class with the standard JPA extension interfaces, and then a low-level adapter with the native Hibernate UserType API.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.56.12