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;
    }
}

Because we are using a bidirectional @OneToMany association, we need to provide the addPricingPlan 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_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?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

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.

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.