Motivation
Most of the time, when we design basic properties of a JPA entity we use easy-to-map types like java.lang.String
, java.math.BigDecimal
, etc.. In this post, I will discuss the the usage of value objects1 instead which are known from the concept of domain driven design2.
I will explain the benefits of using value objects and how to integrate them into JPA entities. Furthermore, this post provides useful hints for DbUnit tests of such entities.
Designing an entity property – first attempt
Let us assume that we want to design an entity which stores the odds for over/under bets3. These odds depend on the total goals we expect to be scored during a football match. So this entity will presumably contain a property expectedTotalGoals
.
We expect the following requirements for this property:
- It must be a numerical type.
- The value it represents must be greater than zero.
- Its scale is always 2.
- If a value with a scale greater than 2 is passed to the setter of this property it should be rounded by mode “half up”.
- If value greater than zero but less than 0.01 is passed to the setter of this property the property should be set to 0.01.
- It should never be null.
Our first design attempt may look like this:
BigDecimal expectedTotalGoals; @Column(name= "expected_total_goals") @NotNull @DecimalMin(value = "0", inclusive = false) public BigDecimal getExpectedTotalGoals() { return this.expectedTotalGoals; } public void setExpectedTotalGoals(BigDecimal expectedTotalGoals) { this.expectedTotalGoals = expectedTotalGoals; }
Semantically, we use estimated total goals in this context. That is why we have decided to name the property expectedTotalGoals
. However, syntactically, we use the type BigDecimal
which represents a superset of the values semantically allowed for estimated total goals.
The requirement 1 is met by choosing the type BigDecimal
. If we persist the entity it will be validated before committing the transaction, thus enforcing the constraints defined by the requirements 2 and 6. Outside the entity we must validate by ourselves that BigDecimal
values representing estimated total goals meet the requirements 2 and 6.
The only way to ensure that a BigDecimal
value fullfills the requirements 3 to 5 is to convert it before passing it to the setter of the property expectedTotalGoals
or to call the converter from within the setter. Again, the value has to be converted anywhere where BigDecimal
values should represent estimated total goals.
Introducing a value object
We replace the BigDecimal
type by a type EstimatedGoals
. This type is immutable and encapsulates its value as a read-only BigDecimal
attribute which is set by the constructor.
public class EstimatedGoals extends Number implements Comparable<EstimatedGoals> { private final BigDecimal value; /** * Creates an instance of {@code EstimatedGoals}. * @param estimatedGoals value representing the statistical goals value * @throws NullPointerException if {@code estimatedGoals} is null * @throws IllegalArgumentException if {@code estimatedGoals} is zero or negative */ private EstimatedGoals(BigDecimal estimatedGoals) { this(estimatedGoals, false); } /** * Creates an instance of {@code EstimatedGoals}. If {@code allowIllegalValues == true} and * {@code estimatedGoals} is zero or negative the instance will be initialized with the smallest, * positive non zero value. * @param estimatedGoals value representing the statistical goals value * @param allowIllegalValues whether illegal values should be allowed * @throws NullPointerException if {@code estimatedGoals} is null * @throws IllegalArgumentException if {@code allowIllegalValues == false} and * {@code estimatedGoals} is zero or negative */ private EstimatedGoals(BigDecimal estimatedGoals, boolean allowIllegalValues) { this.validateLowerBound(estimatedGoals, allowIllegalValues); if (MIN_VALUE.compareTo(estimatedGoals) > 0) { this.value = MIN_VALUE; } else { this.value = estimatedGoals.setScale(ESTIMATED_GOALS_SCALE, RoundingMode.HALF_UP); } } private void validateLowerBound(BigDecimal estimatedGoals, boolean allowIllegalValues) { Objects.requireNonNull(estimatedGoals, "Parameter estimatedGoals must not be null."); if (!allowIllegalValues && MIN_VALUE_ALLOWED.compareTo(estimatedGoals) > 0) { throw new IllegalArgumentException("Parameter estimatedGoals must be a positive non zero value."); } } /** * * @return the {@link BigDecimal] representation of this instance */ public BigDecimal toBigDecimal() { return this.value; } }
The method validateLowerBound
ensures that only valid values are passed to the constructor. The requirements 3 to 5 are met in the conditional block of the constructor.
In this case we can either create a valid EstimatedGoals
instance by logic provided within the class EstimatedGoals
itself or an exception is thrown at construction time. The advantage is that value validation and conversion is only necessary in one place, the constructor of EstimatedGoals
, and nowhere else where we deal with estimated total goals.
In this example, we have provided a second constructor that allows us to correct illegal EstimatedGoals
values. In our own code we can make sure that we only use legal values. However, if we read data from a database it might have been manipulated beyond the control of our code. In that case we can either terminate the loading process by throwing an exception or we replace the corrupt data by possibly intended data.
If we use a value object type the syntactic level of the chosen type (EstimatedGoals
) corresponds to the semantical level of our property.
The class EstimatedGoals
implements the interface Serializable
because it is derived from Number
which implements that interface. The abstract methods of Number
are implemented by delegating their calls to the corresponding calls of this.value
:
@Override public int intValue() { return this.value.intValue(); } @Override public long longValue() { return this.value.longValue(); } @Override public float floatValue() { return this.value.floatValue(); } @Override public double doubleValue() { return this.value.doubleValue(); }
As the equivalence of value objects is determined by its encapsulated attributes1, we add implementations for equals
and hashCode
methods and implement the Comparable
interface:
@Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; EstimatedGoals that = (EstimatedGoals) other; return Objects.equals(value, that.value); } @Override public int hashCode() { return Objects.hash(this.value); } @Override public int compareTo(EstimatedGoals other) { return this.value.compareTo(other.value); }
Again, we delegate comparing objects and creating the hash code to this.value
.
The property of our JPA entity may be designed like this:
private EstimatedGoals expectedTotalGoals; @Column(name= "expected_total_goals") @NotNull public EstimatedGoals getExpectedTotalGoals() { return this.expectedTotalGoals; } public void setExpectedTotalGoals(EstimatedGoals expectedTotalGoals) { this.expectedTotalGoals = expectedTotalGoals; }
Persisting entities with value object properties
JPA 2.1 introduced the AttributeConverter
interface4. We implement that interface to allow conversions between our value object type and the type BigDecimal
which is a supported basic type of JPA.
@Converter(autoApply = true) public class AttributeConverterEstimatedGoals implements AttributeConverter<EstimatedGoals, BigDecimal> { @Override public BigDecimal convertToDatabaseColumn(EstimatedGoals propertyValue) { return propertyValue == null ? null : propertyValue.toBigDecimal(); } @Override public EstimatedGoals convertToEntityAttribute(BigDecimal databaseColumnValue) { return EstimatedGoals.valueOf(databaseColumnValue, true); } }
By adding the autoApply
attribute we can ommit the Convert
annotation5 inside the JPA entity.
The static method EstimatedGoals.valueOf
allows us to handle null and illegal values:
/** * Creates a {@code EstimatedGoals} instance from {@code n}. * @param n value representing the instance to be created * @return a {@code EstimatedGoals} instance for {@code n} * @throws NullPointerException if {@code n} is null * @throws IllegalArgumentException if {@code n} is zero or negative */ public static EstimatedGoals valueOf(Number n) { return valueOf(n, false); } /** * Creates a {@code EstimatedGoals} instance from {@code n}. * @param n value representing the instance to be created * @param allowIllegalValue flag indicating whether null or illegal values for {@code n} should return a * result instead of throwing an exception. If set to true this method will behave this way: * If {@code n} is null null will be returned. * If {@code n} is negative or zero an instance being initialized with the smallest positive non zero value * will be returned. * @return a {@code EstimatedGoals} instance for {@code n} * @throws NullPointerException if {@code allowIllegalValues == false} and {@code n} is null * @throws IllegalArgumentException if {@code allowIllegalValues == false} and * {@code n} is zero or negative */ public static EstimatedGoals valueOf(Number n, boolean allowIllegalValue) { if (allowIllegalValue && n == null) { return null; } BigDecimal estimatedGoals = new BigDecimal(n.toString()); return new EstimatedGoals(estimatedGoals, allowIllegalValue); }
As we call the method valueOf
with the parameter allowIllegalValues
set to true
the method AttributeConverterEstimatedGoals.convertToEntityAttribute
which will be used to convert the value from the database to the property value will behave this way:
- If we pass
null
to the methodnull
will be returned. That way, we can handle nullable properties. - If we pass a value representing zero or a negative value to that method we correct the illegal database value by returning an
EstimatedGoals
instance representing the value 0.01. - Otherwise the
EstimatedGoals
instance representing the value fordatabaseColumnValue
in respect to the requirements 3 to 5 is returned.
Testing entities with value object properties
Entities with value object properties can be tested with DbUnit. I created a test framework inspired by “JUnit in Action”6 which provides an EntityManager
instance and reads the content from a flat xml file to populate an in-memory database.
I had to master some pitfalls though while writing the tests:
Add AttributeConverter
classes to the file “persistence.xml”7
As the tests take place in a JAVA SE context all relevant JPA classes must be enlisted in the file “persistence.xml”. Relevant files are all entity and attribute converter classes.
If you forget to enlist the attribute converters DbUnit tries to serialize/deserialize your value object properties which will result in an exception. If you set the property hibernate.show_sql
to true and forget to register your attribute converter you will notice that DbUnit will create a table column of type varbinary(255)
for the corresponding value object property.
Add precision
and scale
attributes to the Column
annotations8
For numeric columns, DbUnit creates the type numeric(19,2)
by default. This is sometimes not what you want, especially if you need a larger scale. Fortunately, DbUnit honours the precision and scale attributes of the Column
annotation. Therefore, you should use them.
Set uniqueConstraints
attribute inside the Table
annotation9
If you have a unique constraint index in your productive database table you should define it in the Table
annotation of your entity, too. DbUnit honours that attribute and creates such a constraint during database setup. Omitting it will lead to behaviour of your test database different to the productive one.
Conclusion
In this post I showed how to use value object types instead of the easy-to-map types for JPA as entity property types. If value object types are used validation and possibly conversion take place within the constructor of that type only. Each time a value object instance is passed to the property setter it can be considered valid.
The easy-to-map types usually are a superset of the values of the value objects. Therefore, they may represent values which are semantically illegal. If easy-to-map types are used to represent a value validation and possibly conversion have to take place everywhere where this type represents the semantical subset.
Bibliography
- 1.Tactical Design with Aggregates. In: Domain-Driven Design Distilled. Addison-Wesley; 2016:77.
- 2.Evans E. Domain-Driven Design: Tackling Complexity in the Heart of Software. Boston: Addison-Wesley; 2004.
- 3.What Is Over Under Betting? bettingexpert.com. https://www.bettingexpert.com/academy/types-of-betting/over-under-betting. Accessed July 21, 2019.
- 4.Interface AttributeConverter<X,Y>. Java(TM) EE 7 Specification APIs. https://docs.oracle.com/javaee/7/api/index.html?javax/persistence/AttributeConverter.html. Published 2015. Accessed July 21, 2019.
- 5.Annotation Type Convert. Java(TM) EE 7 Specification APIs. https://docs.oracle.com/javaee/7/api/index.html?javax/persistence/Convert.html. Published 2015. Accessed July 21, 2019.
- 6.Testing JPA-based applications. In: JUnit in Action. 2nd ed. Greenwich: Manning; 2011:360-388.
- 7.Registering Converters in JPA 2.1 with EclipseLink. stackoverflow. https://stackoverflow.com/questions/27574855/registering-converters-in-jpa-2-1-with-eclipselink. Published June 3, 2015. Accessed July 21, 2019.
- 8.DbUnit Assertion floating-point numbers. stackoverflow. https://stackoverflow.com/questions/11972579/dbunit-assertion-floating-point-numbers. Published August 15, 2012. Accessed July 21, 2019.
- 9.How to introduce multi-column constraint with JPA annotations? stackoverflow. https://stackoverflow.com/questions/2772470/how-to-introduce-multi-column-constraint-with-jpa-annotations. Published May 5, 2010. Accessed July 21, 2019.