PostgreSQL 函数与存储过程及调用

PostgreSQL 随着云服务的盛行,越发被广泛的应用,免费开源且有丰富的特性支持,加上性能也很不错,因而备受青睐。PostgreSQL 的函数与存储过程区别并不太大,不像某些数据库的函数与存储过程必须是无副作用或有副作用,在 PostgreSQL 的函数和存储过程中可以进行任何的 SQL 操作。简单列举下 PostgreSQL 的函数与存储过程的区别主要如下:

函数

  1. return 或 out 参数返回值,return 可返回单个值或一系列值(return setof 或 return table), 或返回光标(cursor). 函数 return void 就和存储过程差不多了
  2. 函数因其有返回值,所以可通过 select, insert, updata 或 delete 语句来调用,如 select fn1(), delete * from test1 where fn2(c1)=0
  3. 可以用 execute 执行动态 sql, 如 execute 'delete * from ' || 't1'

存储过程

  1. IN, OUT 或 INOUT 参数,但不直接返回值
  2. 不能用 select, insert 等语句使用
  3. 不能用 execute 执行动态 sql

接下来我们来体验一下 PostgreSQL 的函数与存储过程

先用 docker 启动一个 PostgreSQL 数据库,我们使用下面的命令

$ mkdir pgdata
$ docker run -it -v $(PWD)/pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 postgres:15

然后在有 psql 命令的终端执行 $ PGPASSWORD=mysecretpassword psql -h localhost -U postgres 就能连接到该数据库

$ PGPASSWORD=mysecretpassword psql -h localhost -U postgres
psql (16.2, server 15.6 (Debian 15.6-1.pgdg120+2))
Type "help" for help.
postgres=# \c
psql (16.2, server 15.6 (Debian 15.6-1.pgdg120+2))
You are now connected to database "postgres" as user "postgres".
postgres=#

或者用数据库客户端,如 DBeaver 来连接并操作

PostgreSQL 函数

我们不叙述它的完整定义语法,只举几个例子

带 IN 参数和返回单个值

调用

不能通过 call 调用

函数返回一个游标

现在用 select fn2() 得到的是一个 

无法看到游标所代表的结果,需要用 fetch all in "<unnamed portal 1>" 来获得数据,但在 select fn2() 后之后得到游标名称后直接 fetch all 是不行的,如我们再 select fn2();

原因是 select fn2() 之后事物便结束了,<unnamed portal 3> 只存在当前事物中,再次用 fetch all in "<unnamed portal 3>" 它已不存在,需要把 select 和 fetch 包裹在同一个事物当中

begin; 能启动一个事物,在同一个事物中便可获得游标的记录,至于落后用 rollback 或 commit 结束事物都行

如果是用 DB 客户端,如 DBeaver 的话,可设置  Database/Transaction Mode 为 Manual Commit 可以执行  select fn2() 得到游标名后,接着 fetch 结果

命名返回游标

前面的函数返回 refcursor 时,在 select fn2() 时游标名称是依次递增的,如 <unnamed portal 1>, <unnamed portal 2>...., 在 fetch 时必须复制产生的游标名称。我们也可以让函数返回可预知的名称,比如通过参数

那么现在执行 fn3 的游标名称就是明确的,因此可以果断的连续执行下面两句

另一种方式,或者通过 $1 来引用参数

同样的

返回 table 的函数

调用返回 table 的函数无需开启事物

return setof 可以是 setof 任何类型或记录,如 setof varchar, setof record 或 setof refcursor. 像下面的例子

我们创建一个表 t1,然后在 fn6() 函数中返回 record, 并演示函数中的副作用操作(insert)

也是不需要开启事物。

多个 return 返回一系列值的情况

查询

函数的 OUT 参数

函数除了 return 返回值外,可以像存储过程那样用 OUT 参数返回值。其实从最前面的 PostgreSQL 的函数与存储过程来看,功能上存储过程完全没有存在的必要,存储过程能做的函数都能干,函数还能有返回值,可用于 select 等操作,而且还能 execute 动态语句。如果说 PostgreSQL 还有存储过程的一席之地的话,那么只能按照数据库函数与存储过程的约定来选择创建函数还是存储过程

  1. 函数:有返回值,不应产生副作用,不推荐用 OUT 参数返回值
  2. 存储过程:应让它具有副作用,必要时用 OUT 参数返回值

函数的 OUT 参数与 returns 类型是有关联的

一个 OUT 参数时,returns 类型必须与 OUT 参数类型一致

执行时会出错

SQL Error [42P13]: ERROR: function result type must be integer because of OUT parameters

但改成 o out varchar 也不行

仍然出错

SQL Error [42804]: ERROR: RETURN cannot have a parameter in function with OUT parameters
Position: 104

解决的办法是去掉 OUT 参数或者保留 OUT  参数时,在函数体中只能给 OUT 参数赋值

或者

执行效果上都是一样的

是不是感觉 PostgreSQL 的函数与存储过程越发统一起来的;  从某种意义来说,函数可以认为是第一个参数为 OUT 类型的存储过程。函数中返回值即是一个 OUT 参数,函数用 return 语句,存储过程则是为 OUT 类型参数赋值来返回值。

如果是多个 OUT 参数的函数,创建时 PostgreSQL 会要求函数的返回类型为 record, 由此可推断存储过程的 OUT 参数相当于转换成函数后的返回值

带 OUT 参数的存储过程

对存储过程不能用 select, insert 等语句调用了,需用 call

返回每一个 OUT 参数值,OUT 参数可以任何类型,比如 refcursor 游标类型,看下例

OUT 游标类型的参数存储过程

和函数的使用方式差不多,只是改成了 call

返回 void 的函数

我们再由存储过程退回到返回 void 类型的函数

我们要 return void 的函数作什么用呢,唯一的好处就是可以用 select/insert 等语句调用,不用 call

仅此而已。

PostgreSQL 的函数与存储过程那种你泥中有我,我泥中有你的关系,看似方便了用 select 等语句调用,其实却苦了 JDBC 来调用方式

JDBC 在使用函数时可以两种方式

  1. 用 Statement 或 PreparedStatement 像普通 SQL 语句那样 execute(), executeQuery(), executeUpdate(), 或 executeBatch(). 可以 executeBatch() 操作是胜过存储过程的地方
  2. 或用 CallableStatement, 通过 { ? = call fn(?, ?)}, 不关必返回值的话可以像调用存储过程一样 {call fn(?, ?)}

JDBC 使用存储过程要用 CallableStatement 的 call fn(?, ?) 来执行, 注意无需两边的大括号

由于 executeBatch() 是声明在 Statement 类中,executeBatch() 函数只返回受影响的行数,无法获得函数的返回值或存储过程的 OUT 参数。因些,在不关心返回值(函数的返回值或存储过程的 OUT 参数)时, 应该可以 batch 来执行函数和存储过程。

下面是 JDBC 以各种花式调用函数和存储过程的汇总代码, 请对照所调用的 PostgreSQL 函数与存储过程的代码。为完整演示,再补一个表和函数

若忽略该函数的返回值则可当作存储过程来调用

总结几点

  1. select fn() 调用可由 stmt.executeQuery()  得到结果,不管是函数还是存储过程
  2. call.registerOutParameter() 注册的参数须由 call.getXxx() 获得输出值。用了 call xxx 的方式就不能由 stmt.executeQuery() 获得记录集
  3. 无论是调用函数还是存储过程,如果返回值或 OUT 参数是游标(REF_CURSOR) 的话,需开启事物(conn.setAutoCommit(false)
  4. 如果返回值或 OUT 参数不是游标(REF_CURROR),无需开启事物即能获得结果
  5. 调用存储时不要写两边的大括号,即 call pr2(?, ?) ,不能写成 {call pr2(?,?)}
  6. 如果把函数当作存储过程来调用,忽略它的返回值的话,也可以写成 call fn2(), 在用 prepareCall() 时,看是否有 {} 来区别函数还是存储过程
  7. update/insert/delete 可以 batchUpdate, select fn() 也可以 batchUpdate, 如果忽略函数的返回值或调用存储过程时也可以 batchUpdate

如果是使用 JdbcTemplate 的系列 execute(), query(), update(), batchUpdate() 操作的话请参考原始的 JDBC 操作。

本文链接 https://yanbin.blog/postgresql-function-procedure-application/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments