The best way to map MonetaryAmount with JPA and Hibernate
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
In this article, we are going to see what is the best way to map the MonetaryAmount object from Java Money and the Currency API when using JPA and Hibernate.
While the Java Money and Currency API define the specification, like the MonetaryAmount
interface, the Moneta project provides a reference implementation for this API.
Maven Dependency
To use the JavaMoney API in your JPA and Hibernate project, you need to add the following Moneta dependency to your project, which is available on Maven Central:
<dependency> <groupId>org.javamoney</groupId> <artifactId>moneta</artifactId> <version>${moneta.version}</version> <type>pom</type> </dependency>
Domain Model
Let’s assume we have the following Product
and ProductPricing
entities in our system:
The Product
entity can have multiple pricing plans that are represented by the ProductPricing
child entity, as follows:
@Entity(name = "Product") @Table(name = "product") public class Product { @Id private Long id; private String name; @OneToMany( mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true ) private List<ProductPricing> pricingPlans = new ArrayList<>(); public Product addPricingPlan(ProductPricing pricingPlan) { pricingPlans.add(pricingPlan); pricingPlan.setProduct(this); return this; } }
Because we are using a bidirectional
@OneToMany
association, we need to provide theaddPricingPlan
synchronization method as well, as explained in this article.
And the ProductPricing
child entity mapping will use the MonetaryAmountType
to map the price amount and currency.
For Hibernate 6, the mapping will look as follows:
@Entity(name = "ProductPricing") @Table(name = "product_pricing") public class ProductPricing { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) private Product product; private String name; @Enumerated private PricingType type; @AttributeOverride( name = "amount", column = @Column(name = "price_amount") ) @AttributeOverride( name = "currency", column = @Column(name = "price_currency") ) @CompositeType(MonetaryAmountType.class) private MonetaryAmount price; }
And for Hibernate 5, like this:
@Entity(name = "ProductPricing") @Table(name = "product_pricing") @TypeDef(typeClass = MonetaryAmountType.class, defaultForType = MonetaryAmount.class) public class ProductPricing { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) private Product product; private String name; @Enumerated private PricingType type; @Columns(columns = { @Column(name = "price_amount"), @Column(name = "price_currency") }) private MonetaryAmount price; }
The @Type
and @TypeDef
annotations are used to instruct Hibernate to use the MonetaryAmountType
from the Hypersistence Utils to handle the MonetaryAmount
entity attributes.
The MonetaryAmountType
dependency is available on Maven Central, and depending on the Hibernate version you are using, you will have to match the associated dependency to use.
For Hibernate 6, you will need to use
<dependency> <groupId>io.hypersistence</groupId> <artifactId>hypersistence-utils-hibernate-60</artifactId> <version>${hypersistence-utils.version}</version> </dependency>
And for Hibernate 5.6 and 5.5, you need to use this dependency:
<dependency> <groupId>io.hypersistence</groupId> <artifactId>hypersistence-utils-hibernate-55</artifactId> <version>${hypersistence-utils.version}</version> </dependency>
In the Hypersistence Utils GitHub repository, you will find the dependencies to use for other Hibernate 5 versions.
The @ManyToOne
annotation is used to map the product_id
Foreign Key column that references the parent product
record.
The PricingType
is an enumeration that provides the payment strategy for this particular pricing plan, and it can take one of the following two values:
public enum PricingType { ONE_TIME_PURCHASE, SUBSCRIPTION }
The MonetaryAmount
entity attribute uses two @Column
mappings because the price part is going to be stored in the price_amount
column, and the currency will be persisted in the price_currency
column.
Testing Time
When persisting the following Product
entity that has three associated pricing plans using a Fluent style API entity building syntax:
entityManager.persist( new Product() .setId(1L) .setName("Hypersistence Optimizer") .addPricingPlan( new ProductPricing() .setName("Individual License") .setType(PricingType.SUBSCRIPTION) .setPrice( Money.of( new BigDecimal("49.0"), "USD" ) ) ) .addPricingPlan( new ProductPricing() .setName("5-Year Individual License") .setType(PricingType.ONE_TIME_PURCHASE) .setPrice( Money.of( new BigDecimal("199.0"), "USD" ) ) ) .addPricingPlan( new ProductPricing() .setName("10-Dev Group License") .setType(PricingType.SUBSCRIPTION) .setPrice( Money.of( new BigDecimal("349.0"), "USD" ) ) ) );
Hibernate generates the following three SQL INSERT statements:
INSERT INTO product ( name, id ) VALUES ( 'Hypersistence Optimizer', 1 ) INSERT INTO product_pricing ( name, price_amount, price_currency, product_id, type, id ) VALUES ( 'Individual License', 49, 'USD', 1, 1, 1 ) INSERT INTO product_pricing ( name, price_amount, price_currency, product_id, type, id ) VALUES ( '5-Year Individual License', 199, 'USD', 1, 0, 2 ) INSERT INTO product_pricing ( name, price_amount, price_currency, product_id, type, id ) VALUES ( '10-Dev Group License', 349, 'USD', 1, 1, 3 )
Notice that the price
entity attribute is mapped to the price_amount
and price_currency
columns as this entity property is a composite type:
| id | name | price_amount | price_currency | type | product_id | |----|---------------------------|--------------|----------------|------|------------| | 1 | Individual License | 49.00 | USD | 1 | 1 | | 2 | 5-Year Individual License | 199.00 | USD | 0 | 1 | | 3 | 10-Dev Group License | 349.00 | USD | 1 | 1 |
However, the price
attribute is properly instantiated from these two column values, as illustrated by the following example:
ProductPricing pricing = entityManager.createQuery(""" select pp from ProductPricing pp where pp.product.id = :productId and pp.name = :name """, ProductPricing.class) .setParameter("productId", 1L) .setParameter("name", "Individual License") .getSingleResult(); assertEquals( pricing.getPrice().getNumber().longValue(), 49 ); assertEquals( pricing.getPrice().getCurrency().getCurrencyCode(), "USD" );
And, because we are using two columns to store the Money and Currency info, the MonetaryAccountType
works just fine with any relational database, be it Oracle, SQL Server, PostgreSQL, or MySQL.
Cool, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
And there is more!
You can earn a significant passive income stream from promoting all these amazing products that I have been creating.
If you're interested in supplementing your income, then join my affiliate program.
Conclusion
If you want to map a MonetaryAmount Java Object from the Java Money and the Currency API package when using JPA and Hibernate, then the Hypersistence Utils is exactly what you need.
Not only that it provides you with the MonetaryAmountType
, but this solution works with any given relational database, so it will allow you to use the same mappings even if you need to deploy your solution on multiple different database systems.
