Automatic rounding of BigDecimal and Money properties
Description
BigDecimal and Money properties have their scale specified as part of the @IsProperty annotation. Most commonly it is 2, sometimes 4 and very rarely something else.
The underlying database schema is then generated accordingly and can only store the specified number of decimal places. As the result, when a value of 9.395 or 9.3995 is assigned to a property with scale = 2, such entity is saved and then re-retrieved, the value becomes 9.40.
~It would be really useful, helpful and consistent if such values were automatically rounded to the specified scale even before being persisted, as part of the property assignment.~
In order to better control this aspect, it is proposed to:
- [ ] 1. Enhance annotation
IsPropertywith optional parameterroundingMode: RoundingMode, which should only be applicable to properties of typeBigDecimalandMoney. - [ ] 2. Introduce a default validator for properties of type
BigDecimalandMoneyto prevent assignments that lead to the loss of information.
Implementation details
Item 2 requires a bit of unpacking. The main challenge here is that if roundingMode is specified, it is necessary to perform the rounding to the value being assigned so that all pre-conditions (validators), post-conditions (definer), and the actual property assignment would happen using the rounded value.
This can only be achieved by implementing special support for handling properties of type BigDecimal and Money in ObservableMutatorInterceptor. The new logic there would need to check if roundingMode is defined, create a new value with the necessary rounding. Then if the new value is different to the passed value (i.e., rounding actually occurred), pass that new value to the pre-conditions. If pre-conditions succeed, ensure that the new value is assigned to the field, underpinning the property (low-level manipulation would likely be required for this in method proceed). Only then proceed to the processing of post-condition, and the remaining logic.
Expected outcome
Improved productivity, less unexpected errors.
Actual outcome
Confusion and uncertainty in cases where error occur due to rounding as the result of saving/retrieving numbers from a database.
(The argument below was briefly discussed with @elvin-fms. Outlining it here for broader awareness/discussion by the team.)
If entities were immutable then such functionality could indeed be very natural and would even provide an invariant between reading values from an entity fetched from a database and from an entity just having its property value assigned. However, specifically because of the mutable nature of entities, the same property could be assigned multiple times as part of some computation by re-reading its value from the property. Doing so, for example, in a loop would lead to rounding errors.
The following snippet illustrates the problem:
final BigDecimal v = new BigDecimal("0.005");
BigDecimal propValueAsProposed = v.setScale(2, RoundingMode.HALF_EVEN);
BigDecimal propValueAsCurrently = v;
for (int i = 0; i < 10; i++) {
propValueAsCurrently = propValueAsCurrently.add(v);
propValueAsProposed = propValueAsProposed.add(v).setScale(2, RoundingMode.HALF_EVEN);
}
System.out.printf("As proposed: %s%n",propValueAsProposed);
System.out.printf("As currently: %s%n",propValueAsCurrently.setScale(2, RoundingMode.HALF_EVEN));
The output is:
As proposed: 0.00
As currently: 0.06
Unless we can guarantee that any property can be assigned only once during a life of an entity instance (i.e. enforcing immutability, limited to properties and an in-memory instance), implementing this issue would cause more problems than it tries to solve.
@elvin-fms, for reference: During today's discussion, we hypothesised that enforcing a default constraint to prevent the assignment of BigDecimal and Money values with a higher scale than the property declaration, which leads to loss of information, is likely the safest approach.
Using the example from the issue description, assigning a value of 9.395 or 9.3995 to a property with scale = 2 should result in a validation error. However, assigning a value of 9.3900 to such property would pass the validation, resulting in value 9.39.
Potentially, we could extend the IsProperty annotation with optional parameter roundingMode: RoundingMode, which would instruct the default validator that rounding is supported by specifying how exactly rounding should be performed (i.e., HALF_UP, UP, etc.).