Using value object properties in JPA entities

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 objects​1​ instead which are known from the concept of domain driven design​2​.

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 bets​3​. 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:

  1. It must be a numerical type.
  2. The value it represents must be greater than zero.
  3. Its scale is always 2.
  4. 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”.
  5. 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.
  6. 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 attributes​1​, 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 interface​4​. 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 annotation​5​ 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 method null 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 for databaseColumnValue 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 annotations​8​

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 annotation​9​

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. 1.
    Tactical Design with Aggregates. In: Domain-Driven Design Distilled. Addison-Wesley; 2016:77.
  2. 2.
    Evans E. Domain-Driven Design: Tackling Complexity in the Heart of Software. Boston: Addison-Wesley; 2004.
  3. 3.
    What Is Over Under Betting? bettingexpert.com. https://www.bettingexpert.com/academy/types-of-betting/over-under-betting. Accessed July 21, 2019.
  4. 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. 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. 6.
    Testing JPA-based applications. In: JUnit in Action. 2nd ed. Greenwich: Manning; 2011:360-388.
  7. 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. 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. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *