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:

MonetaryAmount JPA and Hibernate Entities

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;
    }
    
    //Getters and setters omitted for brevity
}

Because we are using a bidirectional @OneToMany association, we need to provide the addPricingPlan synchronization method as well, as explained in this article.

The ProductPricing child entity class is mapped 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;

    //Getters and setters omitted for brevity
}

The @TypeDef annotation is used to instruct Hibernate to use the MonetaryAmountType from the Hibernate Types project to handle the MonetaryAmount entity attributes.

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_amountcolumn, 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.

Seize the deal! 40% discount. Seize the deal! 40% discount.

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 Hibernate Types project 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.

Transactions and Concurrency Control eBook

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.