简介
在这篇文章中,我们将看到在使用JPA和Hibernate时,从Java Money和Currency API中映射MonetaryAmount对象的最佳方式是什么。
虽然Java Money和Currency API定义了规范,但像MonetaryAmount 接口,Moneta项目为该API提供了参考实现。
Maven依赖性
要在你的JPA和Hibernate项目中使用JavaMoney API,你需要在你的项目中添加以下Moneta依赖项,该依赖项可在Maven中心找到。
<dependency>
<groupId>org.javamoney</groupId>
<artifactId>moneta</artifactId>
<version>${moneta.version}</version>
<type>pom</type>
</dependency>
领域模型
让我们假设我们的系统中有以下Product 和ProductPricing 实体。

Product 实体可以有多个定价计划,这些计划由ProductPricing 子实体代表,如下所示。
@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
}
因为我们使用的是一个双向的
@OneToMany关联,我们也需要提供addPricingPlan同步方法,正如本文所解释的。
ProductPricing 子实体类是这样映射的。
@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
}
@TypeDef 注解是用来指示Hibernate使用 MonetaryAmountType从Hibernate Types项目中提取的,来处理MonetaryAmount 实体属性。
该 @ManyToOne注解被用来映射product_id 外键列,该列引用了父product 记录。
PricingType 是一个枚举,提供了这个特定定价计划的支付策略,它可以取以下两个值之一。
public enum PricingType {
ONE_TIME_PURCHASE,
SUBSCRIPTION
}
MonetaryAmount 实体属性使用了两个@Column 的映射,因为价格部分要存储在price_amount列中,而货币将被持久化在price_currency 列中。
测试时间
当使用Fluent风格的API实体构建语法持久化以下Product 实体,该实体有三个相关的定价计划。
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生成了以下三个SQL INSERT语句。
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
)
注意,price 实体属性被映射到price_amount 和price_currency 列,因为这个实体属性是一个复合类型。
| 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 |
然而,price 属性被正确地从这两个列的值中实例化了,正如下面的例子所说明的。
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"
);
而且,由于我们使用两个列来存储货币和通货信息,MonetaryAccountType 与任何关系数据库都能正常工作,无论是Oracle、SQL Server、PostgreSQL还是MySQL。
结论
如果你想在使用JPA和Hibernate时从Java Money和Currency API包中映射出一个MonetaryAmount Java对象,那么Hibernate Types项目正是你需要的。
不仅如此,它还为你提供了MonetaryAmountType ,而且这个解决方案适用于任何给定的关系型数据库,所以即使你需要在多个不同的数据库系统上部署解决方案,它也能让你使用相同的映射。