带有运行时参数的 PostgreSQL 视图

news/2024/7/9 19:32:04 标签: postgresql, oracle, 数据库, sql

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

本文旨在通过利用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<Product, Long> { }

@Repository
public interface CurrencyRepository extends CrudRepository<Currency, Long> { }

@Repository
public interface CurrencyExchangeRepository extends CrudRepository<CurrencyExchange, Long> { }

数据源像往常一样在文件中配置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          |+--+-------------------+-----+-----------+

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 更改集中,只要发生更改,该更改集就会运行。​​​​​​​

<?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="sql>postgresql" path="db/changelog/product-view.sql"/>
    </changeSet>
 
</databaseChangeLog>
  • 一个新的实体——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<ProductView, Long> {
 
    List<ProductView> 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<ProductView, Long> {
 
    List<ProductView> 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<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));    }}

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

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


作者:Horatiu Dan

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

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

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


http://www.niftyadmin.cn/n/5240839.html

相关文章

elasticsearch副本和分片

1.文档冲突 当我们使用index API更新文档&#xff0c;可以一次性读取 修改索引副本 rootes-node3:~# curl -XPUT http://192.168.1.136:9200/es-syslog-2023.08.26/_settings -H "Content-Type: application/json" -d { > "settings": { > …

国产Type-C接口逻辑协议芯片:Type-C显示器芯片方案

产品介绍 双Type-C盲插选型&#xff1a; LDR6282 PD3.0认证协议芯片&#xff0c;USB-IF TID号&#xff1a;212 支持iic&#xff0c;USB转UART&#xff0c;CC升级方式&#xff0c;多年市场验证&#xff0c;显示器市场出货量&#xff0c;显示器大厂采用兼容性NO.1。采用QFN32 5…

【CentOS】配置 Apache 服务

yum install httpd -y# 查看是否安装成功 httpd -v # 出现版本号表示成功# 启动服务 systemctl start httpd# 查看状态 systemctl status httpd # running 即可成功 ● httpd.service - The Apache HTTP ServerLoaded: loaded (/usr/lib/systemd/system/httpd.service; disable…

[EFI]Atermiter X99 Turbo D4 E5-2630v3电脑 Hackintosh 黑苹果efi引导文件

硬件型号驱动情况主板 Atermiter X99 Turbo D4 处理器 Intel Xeon E5-2630v3 已驱动内存Desktop DDR4 2666 MHz已驱动硬盘Netac NV7000已驱动显卡AMD Radeon RX 5700xt已驱动声卡瑞昱 英特尔 High Definition Audio 控制器ALC897已驱动网卡LucyRTL8125已驱动无线网卡蓝牙Broad…

PWM控制器电路D9741,定时闩锁、短路保护电路,输出基准电压(2.5V) 采用SOP16封装形式

D9741是一块脉宽调制方三用于也收路像机和笔记本电的等设备上的直流转换器。在便携式的仪器设备上。 主要特点&#xff1a;● 高精度基准电路 ● 定时闩锁、短路保护电路 ● 低电压输入时误操作保护电路 ● 输出基准电…

HarmonyOS学习--了解基本工程目录

1.工程级目录 工程的目录结构如下&#xff1a; 其中详细如下&#xff1a; AppScope中存放应用全局所需要的资源文件。entry是应用的主模块&#xff0c;存放HarmonyOS应用的代码、资源等。oh_modules是工程的依赖包&#xff0c;存放工程依赖的源文件。build-profile.json5是工…

redis整理

1. 数据类型 string , hash, 链表&#xff0c;Set, ZSet. string 底层是sds, sds与普通字符串的区别: a. sds存储了字符串长度&#xff0c;获取长度的时间复杂度为O(1); b. sds操作字符串会预先判断长度是否满足要求, 不会有字符串溢出的情况出现; c. 提前预分配, 惰性回收…

在UBUNTU上使用Qemu和systemd-nspawn搭建RISC-V轻量级用户模式开发环境

参考链接 使用Qemu和systemd-nspawn搭建RISC-V轻量级用户模式开发环境 - 知乎 安装Qemu sudo apt updatesudo apt -y install qemu-user-binfmt qemu-user-static systemd-container sudo apt -y install zstd 配置环境 RISCV_FILEarchriscv-2023-10-09.tar.zstwget -c ht…