有许多情况下,应用程序需要灵活多变,能够在运行时生成动态报告。
本文旨在通过利用 PostgreSQL 数据库支持的临时配置参数,提供一种实现这一目标的方法。
根据 PostgreSQL 文档,在 7.3 版本开始,可以使用 set_config(name, value, is_local) 函数设置配置参数。稍后,可以使用 current_setting(name) 函数读取先前设置的参数值,必要时进行转换并使用。如果前一个函数的第三个参数为 true,则更改的设置仅适用于当前事务。
这正是这里所需要的——提供一个可以作为原子操作的一部分使用的运行时参数值的方法。
示例应用程序构建如下:
在应用程序级别,Maven 项目配置为使用 Spring Data JPA 和 Liquibase 依赖项。
领域由产品表示,其价格以各种货币表示。为了在不同货币之间进行转换,存在货币汇率。目标是能够以某种货币的汇率读取所有产品及其价格,以及某一天的汇率。
为了开始建模,连接到数据库后首先应创建一个新模式。
create schema pgsetting;
有三个实体:Product、Currency 和 CurrencyExchange。
@Entity@Table(name = "product")public class Product { @Id @Column(name = "id") private Long id; @Column(name = "name", nullable = false) private String name; @Column(name = "price", nullable = false) private Double price; @ManyToOne @JoinColumn(name = "currency_id") private Currency currency; ...}@Entity@Table(name = "currency")public class Currency { @Id @Column(name = "id", nullable = false) private Long id; @Column(name = "name", nullable = false) private String name; ...}@Entity@Table(name = "currency_exchange")public class CurrencyExchange { @Id @Column(name = "id", nullable = false) private Long id; @Column(name = "date", nullable = false) private LocalDate date; @ManyToOne @JoinColumn(name = "from_currency_id", nullable = false) private Currency from; @ManyToOne @JoinColumn(name = "to_currency_id", nullable = false) private Currency to; @Column(name = "value", nullable = false) private Double value; ...}
每个实体都有相应的 CrudRepository。
@Repositorypublic interface ProductRepository extends CrudRepository<Product, Long> { }@Repositorypublic interface CurrencyRepository extends CrudRepository<Currency, Long> { }@Repositorypublic interface CurrencyExchangeRepository extends CrudRepository<CurrencyExchange, Long> { }
数据源通常在 [application.properties](
https://github.com/horatiucd/pg-setting/blob/master/src/main/resources/application.properties) 文件中配置,其中包括记录了用于初始化模式的三个表和它们之间关系的 Liquibase 变更日志文件的路径。
有关详细信息,可以查看应用程序属性和
db/changelog/schema-init.xml 文件。
根变更日志文件为:
<?xml version="1.1" encoding="UTF-8" standalone="no"?><databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"> <include file="/db/changelog/schema-init.xml"/></databaseChangeLog>
应用程序启动时,变更集按声明顺序执行。到目前为止,一切都很简单,没有什么特别之处——一个简单的 Spring Boot 应用程序,其数据库变更由 Liquibase 管理。
假设当前应用程序定义了两种货币——RON 和 EUR,以及两种以不同货币记录价格的产品。
+--+----+|id|name|+--+----+|1 |RON ||2 |EUR |+--+----+
+--+-------------------+-----+-----------+|id|name |price|currency_id|+--+-------------------+-----+-----------+|1 |Swatch Moonlight v1|100 |2 ||2 |Winter Sky |1000 |1 |+--+-------------------+-----+-----------+
+--+----------+----------------+--------------+-----+|id|date |from_currency_id|to_currency_id|value|+--+----------+----------------+--------------+-----+|1 |2023-11-15|2 |1 |5 ||2 |2023-11-15|2 |2 |1 ||3 |2023-11-15|1 |2 |0.2 ||4 |2023-11-15|1 |1 |1 |+--+----------+----------------+--------------+-----+
目标结果是一个产品报告,其中所有价格以 EUR 表示,使用 2023 年 11 月 15 日的汇率。这意味着需要转换第二个产品的价格。
为了简化设计,先前设定的目标被分解为更小的部分,然后逐一实现。从概念上讲,应首先获取产品,然后转换它们的价格(如果需要)。
前者很简单。Spring Data Repository 方法很容易允许获取产品——List<Product> findAll()。
后者可以通过查询实现转换。
SELECT p.id, p.name, p.price * e.value price, e.to_currency_id currency_id, e.dateFROM product pLEFT JOIN currency_exchange e on p.currency_id = e.from_currency_id and e.to_currency_id = 2 and e.date = '2023-11-15'
为了将两者合并,完成了以下操作:
它在 product-view.sql 文件中定义,并作为可重复的 Liquibase 变更集的幂等操作添加,每当更改时运行。
<?xml version="1.1" encoding="UTF-8" standalone="no"?><databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"> <include file="/db/changelog/schema-init.xml"/> <changeSet id="repeatable" author="horatiucd" runOnChange="true"> <sqlFile dbms="postgresql" path="db/changelog/product-view.sql"/> </changeSet></databaseChangeLog>
@Entity@Immutablepublic class ProductView { @Id private Long id; private String name; private Double price; private LocalDate date; @ManyToOne @JoinColumn(name = "currency_id") private Currency currency; ...}
@Repositorypublic interface ProductViewRepository extends org.springframework.data.repository.Repository<ProductView, Long> { List<ProductView> findAll();}
现在应用程序能够构建所需的报告,但只能为硬编码的货币和汇率。
为了在运行时传递这两个值,同一事务中执行了以下操作:
此外,修改了 product_view,以读取作为当前事务的一部分设置的配置参数,并相应地选择数据。
SELECT p.id, p.name, p.price * e.value price, e.date, e.to_currency_id currency_idFROM product pLEFT JOIN currency_exchange e on p.currency_id = e.from_currency_id and e.to_currency_id = current_setting('pgsetting.CurrencyId')::int and e.date = current_setting('pgsetting.CurrencyDate')::date;
current_setting('pgsetting.CurrencyId') 和 current_setting('pgsetting.CurrencyDate') 调用读取先前设置的参数,然后进行转换并使用。
实现需要一些额外的调整。
ProductViewRepository 增加了一个允许设置配置参数的方法。
@Repositorypublic interface ProductViewRepository extends org.springframework.data.repository.Repository<ProductView, Long> { List<ProductView> findAll(); @Query(value = "SELECT set_config(:name, :value, true)") void setConfigParam(String name, String value);}
最后一个参数始终设置为 true,因此该值仅在当前事务期间保留。
此外,定义了一个 ProductService,用于清楚地标记事务中涉及的所有操作。
@Servicepublic class ProductService { private final ProductViewRepository productViewRepository; public ProductService(ProductViewRepository productViewRepository) { this.productViewRepository = productViewRepository; } @Transactional public List<ProductView> getProducts(Currency currency, LocalDate date) { productViewRepository.setConfigParam("pgsetting.CurrencyId", String.valueOf(currency.getId())); productViewRepository.setConfigParam("pgsetting.CurrencyDate", DateTimeFormatter.ofPattern("yyyy-MM-dd").format(date)); return productViewRepository.findAll(); }}
参数的名称与 product_view 定义中使用的名称相同。
为了验证实现,设置了两个测试。
@SpringBootTestclass Product1Test { @Autowired private CurrencyRepository currencyRepository; @Autowired private ProductRepository productRepository; @Autowired private CurrencyExchangeRepository rateRepository; @Autowired private ProductService productService; private Currency ron, eur; private Product watch, painting; private CurrencyExchange eurToRon, ronToEur; private LocalDate date; @BeforeEach public void setup() { ron = new Currency(1L, "RON"); eur = new Currency(2L, "EUR"); currencyRepository.saveAll(List.of(ron, eur)); watch = new Product(1L, "Swatch Moonlight v1", 100.0d, eur); painting = new Product(2L, "Winter Sky", 1000.0d, ron); productRepository.saveAll(List.of(watch, painting)); date = LocalDate.now(); eurToRon = new CurrencyExchange(1L, date, eur, ron, 5.0d); CurrencyExchange eurToEur = new CurrencyExchange(2L, date, eur, eur, 1.0d); ronToEur = new CurrencyExchange(3L, date, ron, eur, .2d); CurrencyExchange ronToRon = new CurrencyExchange(4L, date, ron, ron, 1.0d); rateRepository.saveAll(List.of(eurToRon, eurToEur, ronToEur, ronToRon)); }}
前者获取以 EUR 记录价格,使用记录的汇率。
@Testvoid prices_in_eur() { List<ProductView> products = productService.getProducts(eur, date); Assertions.assertEquals(2, products.size()); Assertions.assertTrue(products.stream() .allMatch(product -> product.getCurrency().getId().equals(eur.getId()))); Assertions.assertTrue(products.stream() .allMatch(product -> product.getDate().equals(date))); Assertions.assertEquals(watch.getPrice(), products.get(0).getPrice()); Assertions.assertEquals(painting.getPrice() * ronToEur.getValue(), products.get(1).getPrice());}
调用时,product_view 为:
+--+-------------------+-----+-----------+----------+|id|name |price|currency_id|date |+--+-------------------+-----+-----------+----------+|1 |Swatch Moonlight v1|100 |2 |2023-11-15||2 |Winter Sky |200 |2 |2023-11-15|+--+-------------------+-----+-----------+----------+
后者使用相同的汇率,获取以 RON 价格的产品。
@Testvoid prices_in_ron() { List<ProductView> products = productService.getProducts(ron, date); Assertions.assertEquals(2, products.size()); Assertions.assertTrue(products.stream() .allMatch(product -> product.getCurrency().getId().equals(ron.getId()))); Assertions.assertTrue(products.stream() .allMatch(product -> product.getDate().equals(date))); Assertions.assertEquals(watch.getPrice() * eurToRon.getValue(), products.get(0).getPrice()); Assertions.assertEquals(painting.getPrice(), products.get(1).getPrice());}
调用时,product_view 为:
+--+-------------------+-----+-----------+----------+|id|name |price|currency_id|date |+--+-------------------+-----+-----------+----------+|1 |Swatch Moonlight v1|500 |1 |2023-11-15||2 |Winter Sky |1000 |1 |2023-11-15|+--+-------------------+-----+-----------+----------+
可在 此处 找到。