Custom Development

Tweaking Persistence with @PrePersist

Mitch Goldstein

It is an interesting problem: even though the Java primitive double type has plenty of precision, math operations using this type can prove problematic when it comes to using them for precision calculations. Due to the nature of how floating point numbers are stored, they are by definition inaccurate. This is a consequence of trying to store decimals - especially fractional decimals - in a binary format.

To address some of these issues, the Java development team released the java.math.BigDecimal class. This is an immutable class that extends the abstract class java.lang.Number, which serves as the superclass of the primitive wrapper classes such as java.lang.Double and java.lang.Integer. The primary purpose of this base class is to provide methods that allow conversion among different number types.

BigDecimal is supported by the Java Persistence API (JPA), which uses Java Annotations to integrate Java objects with relational entities. This makes BigDecimal very tempting to use in lieu of floating-point primitives, since the JPA code, in conjunction with the JDBC driver, automatically converts persisted numeric values to BigDecimal when they are mapped to numeric table columns. However, this can be problematic depending on the type of database you happen to be connecting to.

The Problem

I encountered a problem where an object I wanted to persist was throwing a JDBC exception. It turned out that the problem was caused by the way the JDBC driver handles BigDecimal properties. This code illustrates this peculiarity:

BigDecimal bd = new BigDecimal(48.28);
System.out.println("toString() = " + bd.toString());
System.out.println("doubleValue() = " + bd.doubleValue());

<strong>Output:
toString()    = 48.280000000000001136868377216160297393798828125
doubleValue() = 48.28</strong>

Was this what you might have expected? It looks like the interpretation of the value is correct, but the string representation seems to have several decimal places of precision followed by random junk! Little did I know that this would wreak havoc in my code. Apparently, my JDBC driver converts the value to a string, which can usually, but not always, be digested. I would get a sporadic exception such as this when I tried to persist this data:

JDBCExceptionReporter:234 - Error converting data type nvarchar to decimal.

So what is the problem? Here we have a monetary value with two decimal places of precision. We want to use BigDecimal to eliminate rounding errors, and we can't get it into the database?

The Solution

Regrettably, we don't have control over the internal workings of JPA or the associated JDBC drivers, so we have to work with what we have. What we really need to do is to figure out how to modify the precision of the BigDecimal so it still behaves the way we want, but doesn't cause the code in the driver to break.

Since BigDecimal is immutable, we really can't fiddle with the values themselves. Much like the analogous String class, operations which alter values all return a fresh String object with the desired changes. There are operations that can be used to adjust the scale of the BigDecimal so that its raw string representation is not quite so cumbersome.
BigDecimal bd = new BigDecimal(48.28);
bd = bd.setScale(10, RoundingMode.HALF_EVEN);
System.out.println("toString() = " + bd.toString());
System.out.println("doubleValue() = " + bd.doubleValue());

Output:
toString()    = 48.2800000000
doubleValue() = 48.28

Even so - if you are using JPA to it's fullest extent, it is quite likely that your mappings are applied not to your class's methods, but directly to a class field, such as this declaration:
@Column (name = "PRICE")
protected BigDecimal _price;

This basically means you cannot 'trap' the update of this field in a method call when JPA uses reflection to update this value - how can we ensure our values are persistable if we can't interrupt the persistance process?

The @PrePersist Annotation

The developers of JPA anticipated this issue and provided a very useful way to invoke code when an object is just about to be persisted. Using reflection, the persistence engine will look for any methods within the entity that is annotated with the @PrePersist annotation. This provides an excellent hook to either examine or modify entity values before they are persisted by JPA. In the case of the 'price' field above, we could add a method to tweak the value of the price to a more reasonable value before we try to insert the value in a table.

    @PrePersist
    protected void prePersist()
    {
        _price = _price.setScale(10, RoundingMode.HALF_EVEN);
    }

This method will get invoked just prior to our object getting persisted, and will replace the 'price' value with its equivalent value, adjusting it's scale so that it is still very precise, but will not cause problems due to string length when it is persisted to a relational store.

The second parameter of the setScale() method gives the rounding strategy that is used for the conversion. Any time the scale of a BigDecimal is modified, the user must provide the rule by which the scale will be reduced. Here I chose the 'half-even' method, which is also the one used by applications that support numeric analysis (such as Excel) since it provides a more even distribution of discrepancies over larger sets of data. For more information, have a look at the documentation for the java.math.RoundingMode enumeration.

Further study of the Java Persistence API will uncover many other 'callback' annotations that can be used to provide finer control over entity life-cycle without having to write large amounts of custom code.

Mitch Goldstein
ABOUT THE AUTHOR

Senior Technical Consultant Mitch Goldstein is Summa's first Technical Agile Coach as well as a certified SAFe Program Consultant, combining centuries of technical experience and a great love for all things lean and agile. Mitch has been a featured speaker at technical conferences in the US and Europe as well as a published author and technical journalist. Try to imagine a polymath, but only up to the easy part of calculus. Dabbles in Freemasonry.