用JPA和Hibernate映射MonetaryAmount的最佳方法

402 阅读3分钟

简介

在这篇文章中,我们将看到在使用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>

领域模型

让我们假设我们的系统中有以下ProductProductPricing 实体。

MonetaryAmount JPA and Hibernate Entities

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使用 MonetaryAmountTypeHibernate 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_amountprice_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 ,而且这个解决方案适用于任何给定的关系型数据库,所以即使你需要在多个不同的数据库系统上部署解决方案,它也能让你使用相同的映射。