带有运行时参数的 PostgreSQL 视图

在许多情况下,应用程序需要足够灵活和多功能,以便能够运行动态报告,其中输入在运行时提供。

本文旨在通过利用PostgreSQL数据库支持的临时配置参数来展示如何实现这一点。

根据PostgreSQL文档,从7.3版本开始,可以使用set_config(name, value, is_local)函数设置配置参数。随后,可以使用current_setting(name)函数读取先前设置参数的值,必要时进行转换,并使用它。如果前一个函数的第三个参数为true,则更改的设置仅适用于当前事务。

这正是这里需要的 —— 一种提供可以作为原子操作一部分使用的运行时参数值的方法。

设置 

示例应用程序的构建包括:

  • Java 21

  • Spring Boot 版本 3.1.15

  • PostgreSQL 驱动版本 42.6.0

  • Liquibase 4.20.0

  • Maven 3.6.3

在应用程序级别,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。


@Repository
public interface ProductRepository extends CrudRepository { }

@Repository
public interface CurrencyRepository extends CrudRepository { }

@Repository
public interface CurrencyExchangeRepository extends CrudRepository { }

数据源像往常一样在文件中配置application.properties,以及 Liquibase 更改日志文件的路径,该文件记录了一些简单的更改集,用于使用三个表及其之间的关系初始化架构。

有关详细信息,可以探索应用程序属性和db/changelog/schema-init.xml文件。

根变更日志文件是:





    


当应用程序启动时,更改集按照声明的顺序执行。到目前为止,一切都很简单,没有什么异常——一个简单的 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          |+--+-------------------+-----+-----------+

11 月 15 日 货币汇率​​​​​​​

+--+----------+----------------+--------------+-----+|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    |+--+----------+----------------+--------------+-----+

目标结果是一份产品报告,其中所有价格均以欧元为单位,使用 2023 年 11 月 15 日起的汇率。这意味着需要转换第二个产品的价格。

为了简化设计,之前设定的目标被分成更小的部分,然后被克服。从概念上讲,应获取产品并转换其价格(如果需要)。

  1. 获取产品。

  2. 使用请求日期的汇率以请求的货币转换价格。

前者是微不足道的。Spring Data Repository 方法可以轻松获取产品 - List 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

它在product-view.sql文件中定义,并作为幂等操作添加到可重复的Liquibase 更改集中,只要发生更改,该更改集就会运行。​​​​​​​



 
    
 
    
        
    
 
  • 一个新的实体——ProductView连同相应的存储库一起被定义为域的一部分。

@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;         ...}
@Repository
public interface ProductViewRepository extends org.springframework.data.repository.Repository {
 
    List findAll();
}

该应用程序现在能够构建所需的报告,但仅限于硬编码的货币和汇率。

为了在运行时传递两者,在同一事务中执行以下操作:

  • 这两个参数值被设置为配置参数 -SELECT set_config(:name, :value, true)

  • ProductView使用存储库方法获取实体

此外,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通过允许设置配置参数的方法进行了增强。

@Repository
public interface ProductViewRepository extends org.springframework.data.repository.Repository {
 
    List findAll();
 
    @Query(value = "SELECT set_config(:name, :value, true)")
    void setConfigParam(String name, String value);
}

最后一个参数始终设置为true,因此该值仅在当前事务期间保留。

另外,ProductService定义a是为了清楚地标记事务中涉及的所有操作。​​​​​​​

@Servicepublic class ProductService {     private final ProductViewRepository productViewRepository;     public ProductService(ProductViewRepository productViewRepository) {        this.productViewRepository = productViewRepository;    }     @Transactional    public List 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));    }}

前者使用记录的汇率获取欧元价格的产品。​​​​​​​

@Testvoid prices_in_eur() {    List 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 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|+--+-------------------+-----+-----------+----------+


作者:Horatiu Dan

更多技术干货请关注公号【云原生数据库

squids.cn,云数据库RDS,迁移工具DBMotion,云备份DBTwin等数据库生态工具。

irds.cn,多数据库管理平台(私有云)。

你可能感兴趣的:(技术专栏,postgresql,oracle,数据库,sql)