带有运行时参数的 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依赖。

该领域由产品组成,其价格以不同货币表示。为了进行货币之间的转换,存在货币汇率。目标是能够以某种货币表示的价格读取所有产品,按照某一天的汇率。

概念证明

为了开始建模,首先在连接到数据库后创建一个新的模式。

ini 复制代码
create schema pgsetting;

共有三个实体:Product、Currency和CurrencyExchange。

less 复制代码
@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。

less 复制代码
@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 复制代码
<?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,以及两种产品,其价格以不同的货币记录。

货币

diff 复制代码
+--+----+
|id|name|
+--+----+
|1 |RON |
|2 |EUR |
+--+----+

产品

diff 复制代码
+--+-------------------+-----+-----------+
|id|name               |price|currency_id|
+--+-------------------+-----+-----------+
|1 |Swatch Moonlight v1|100  |2          |
|2 |Winter Sky         |1000 |1          |
+--+-------------------+-----+-----------+

11 月 15 日 货币汇率

lua 复制代码
+--+----------+----------------+--------------+-----+
|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().

后者可以通过进行转换的查询来实现。

css 复制代码
SELECT p.id,
       p.name,
       p.price * e.value price,       
       e.to_currency_id currency_id,
       e.date
FROM product p
LEFT 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 复制代码
<?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>
  • 一个新的实体------ProductView连同相应的存储库一起被定义为域的一部分。
kotlin 复制代码
@Entity
@Immutable
public class ProductView {
 
    @Id
    private Long id;
 
    private String name;
 
    private Double price;
 
    private LocalDate date;
 
    @ManyToOne
    @JoinColumn(name = "currency_id")
    private Currency currency;
     
    ...
}
csharp 复制代码
@Repository
public interface ProductViewRepository extends org.springframework.data.repository.Repository<ProductView, Long> {
 
    List<ProductView> findAll();
}

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

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

  • 这两个参数值被设置为配置参数 -SELECT set_config(:name, :value, true)
  • ProductView使用存储库方法获取实体

此外,product_view修改为读取作为当前事务的一部分设置的配置参数并相应地选择数据。

css 复制代码
SELECT p.id,
       p.name,
       p.price * e.value price,
       e.date,
       e.to_currency_id currency_id
FROM product p
LEFT 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通过允许设置配置参数的方法进行了增强。

java 复制代码
@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是为了清楚地标记事务中涉及的所有操作。

kotlin 复制代码
@Service
public 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。

为了验证实施情况,设置了两项测试。

ini 复制代码
@SpringBootTest
class 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));
    }
}

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

less 复制代码
@Test
void 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是:

lua 复制代码
+--+-------------------+-----+-----------+----------+
|id|name               |price|currency_id|date      |
+--+-------------------+-----+-----------+----------+
|1 |Swatch Moonlight v1|100  |2          |2023-11-15|
|2 |Winter Sky         |200  |2          |2023-11-15|
+--+-------------------+-----+-----------+----------+

后者使用相同的汇率获取价格为 RON 的产品。​​​​​​​

less 复制代码
@Test
void 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是:

lua 复制代码
+--+-------------------+-----+-----------+----------+
|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,多数据库管理平台(私有云)。

相关推荐
uzong6 分钟前
软件架构指南 Software Architecture Guide
后端
又是忙碌的一天6 分钟前
SpringBoot 创建及登录、拦截器
java·spring boot·后端
萧曵 丶14 分钟前
事务ACID特性详解
数据库·事务·acid
kejiayuan1 小时前
CTE更易懂的SQL风格
数据库·sql
kaico20181 小时前
MySQL的索引
数据库·mysql
勇哥java实战分享1 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
学历真的很重要1 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪1 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端
清水白石0082 小时前
解构异步编程的两种哲学:从 asyncio 到 Trio,理解 Nursery 的魔力
运维·服务器·数据库·python