我想要在Fortran中使用数据库!
简而言之
PostgreSQL具有用于前端的C库libpq。借助现代Fortran的C互操作功能,我们创建了一个Fortran封装程序Libpq-Fortran,并介绍其用法。
Libpq-Fortran — Libpq的现代Fortran接口
引入
在关系数据库管理系统中,有多种实现方法。例如MySQL、PostgreSQL、IBM Db2等都是我们能想到的。此外,还有适用于轻量级应用的SQLite。
根据Philipp Engel先生的说法(参考文献1),有几个Fortran模块与SQLite相关。以下有关这四个库的总结。
Fortran-SQLite3是以纯Fortran 2018编写的现代接口绑定到SQLite 3的工具。
FLIBS是Fortran 90模块,包含围绕SQLite的非标准包装例程。
libGPF是一个通用的Fortran集合,包括SQLite 3的绑定。
sqliteff是Fortran 2003的SQLite版本,它是SQLite库的一个薄C包装。
《SQLite – 使用现代Fortran编程》是由Philipp Engel撰写的书籍。
然而,这些只是数据库的“库”,并不能提供访问数据库“服务器”的手段。虽然引入的难度可能会比较高,但如果需要处理相当大规模的数据,也会有一些情况需要在服务器上进行管理。
因此,我决定开发一个作为PostgreSQL数据库服务器客户端接口的Fortran模块。我将它在GitHub上以”Libpq-Fortran”的名称公开(采用MIT许可证)。
Libpq-Fortran是使用Fortran的C互操作功能,可以访问PostgreSQL的官方C库libpq。虽然该软件的文档尚未准备好,但在本文中,我想介绍它的初始用法。
依存关系软件
在本部分中,将介绍Libpq-Fortran所依赖的软件。
构建所需的软件如下所示。
-
- libpq
-
- Fortranコンパイラ
- Fortranパッケージマネージャー(fpm)
首先,您需要在本地机器的系统上安装libpq库。在Windows上,当您在向导中安装PostgreSQL时,它会自动安装。具体来说,对于PostgreSQL 15版本,只需确保存在文件C:\Program Files\PostgreSQL\15\include\libpq-fe.h即可。对于Ubuntu,您可以运行sudo apt install libpq-dev命令进行安装。
接下来需要一个Fortran编译器。目前,Libpq-Fortran支持以下编译器的构建。
-
- GNU Compiler Collection: gfortran
Intel oneAPI HPC toolkit: Fortran Compiler ifx, Fortran Compiler Classic ifort
还需要Fortran软件包管理器(fpm)。您可以从GitHub上下载它。另外,还需要我的另一个库uint-fortran,但是fpm会自动处理它的依赖关系。
最后需要连接到PostgreSQL数据库服务器。这个服务器必须可通过本地计算机或本地网络访问。请准备一个不会有数据丢失问题的数据库,用于进行此软件的测试。
建造
首先,从bash或powershell等终端执行以下命令来构建Libpq-Fortran。
$ git clone https://github.com/shinobuamasaki/libpq-fortran
$ cd libpq-fortran
$ fpm build
当执行fpm build时,有时需要告诉编译器“libpq-fe.h”文件的目录路径。在这种情况下,可以将路径写在环境变量FPM_CFLAGS或构建选项–c-flag后面的-I后面。
举个例子,以Ubuntu为例,该目录可能是/usr/include/postgresql,所以命令应该是如下所示。
$ export FPM_CFLAGS="-I/usr/include/postgresql"
$ fpm build
または
$ fpm build --c-flag "-I/usr/include/postgresql"
试用Libpq-Fortran。
在存储库的example目录中,包含了一个程序,可以通过与PostgreSQL服务器进行交互,获取服务器数据库列表的数据。
要执行这个,需要使用如下的fpm run命令。
$ fpm run demo --example
demo.f90 done.
demo done.
[100%] Project compiled successfully.
=== INPUT Database Information ===
=== type "q" for quit ===
Hostname:
如果出现要求输入关于访问服务器的信息的主机名提示:Hostname:,那就表示成功了。
只要像下面的例子输入信息,就可以建立与数据库的连接并提取结果。(请根据各自的环境来修改主机名、数据库名、用户名和密码)。
=== INPUT Database Information ===
=== type "q" for quit ===
Hostname: localhost
dbname: postgres
user: shinobu
password: xxxyyyzzz
=== END INPUT ===
=== Query Result===
tuples, fields: 3 1
=== Available database names ===
postgres
template1
template0
如果在Available database names下面至少显示出postgres、template1、template0这三个名称,那么就表示成功了。在使用Fortran编写的代码的帮助下,我们成功实现了对数据库服务器的访问。接下来的部分将详细介绍我们所使用的代码。
演示程序
在这个部分,我们将详细说明上面例子中使用的demo.f90程序。整个程序可以在本文的附录或GitHub上查看。
宣言部 bù)
這個程式的宣告語句如下:
program demo
use :: libpq
use, intrinsic :: iso_c_binding
use, intrinsic :: iso_fortran_env, only:stdout=>output_unit, stdin=>input_unit
type(c_ptr) :: conn, res
character(:, kind=c_char), allocatable :: query, conninfo
integer :: i, port
character(256) :: str, host, dbname, user, password
请注意这里的use :: libpq声明。这个模块是Libpq-Fortran的核心模块。在Libpq-Fortran中,通过声明使用该模块,可以从Fortran中使用libpq的过程。
接下来有use, intrinsic::iso_c_binding的声明。之所以声明这个是因为该软件具有C库的直接接口,因此需要进行C样式的编程,即接收对象指针作为函数返回值并将其传递,type(c_ptr)::conn, res是为了在这种风格下编写程序而声明的C指针变量。
执行部
接下来我们来看一下执行语句。
print *, '=== INPUT Database Information ==='
print *, '=== type "q" for quit ==='
write (stdout, '(a)', advance='no') "Hostname: "
read (stdin, *) host
if (host == 'q') stop
port = 5432
write (stdout, '(a)', advance='no') "dbname: "
read (stdin, *) dbname
if (dbname == 'q') stop
write (stdout, '(a)', advance='no') "user: "
read (stdin, *) user
if (user == 'q') stop
write (stdout, '(a)', advance='no') "password: "
read (stdin, *) password
if (password == 'q') stop
print *, "=== END INPUT ==="
write(str, '(a, i0, a)') &
"host="//trim(adjustl(host))// &
" port=", port, &
" dbname="//trim(adjustl(dbname))// &
" user="//trim(adjustl(user))// &
" password="//trim(adjustl(password))
conninfo = str
这部分代码负责接收连接信息的输入。它将用户的信息输入收集为一个固定长度的字符串,并将其复制到一个可变长度的字符串变量中作为PostgreSQL连接字符串。
在下一行执行的是连接到数据库的操作。
conn = PQconnectdb(conninfo)
if (PQstatus(conn) /= 0) then
print *, PQerrorMessage(conn)
error stop
end if
使用参数conninfo调用函数PQconnectdb,并将结果赋值给C指针类型变量conn。这个指针将作为连接信息的标识符供用户使用,但无需考虑这个对象的内部结构。紧随其后的if块是错误处理。如果无法建立连接,将显示错误消息并结束程序。
进一步编写并执行SQL代码的代码会出现。
query = "select datname from pg_database;"
res = PQexec(conn, query)
if (PQstatus(conn) /= 0 ) then
print *, PQerrorMessage(conn)
end if
首先,我们将SQL查询语句写入字符串变量query。然后,将query和conn作为参数传递给函数PQexec,并将结果存储在C指针变量res中。这个SQL查询的意思是“从服务器上的数据库集合中获取数据库名称的列表”。接下来的if块是与之前一样的错误处理。
然后,它将进入从res对象中提取结果数据的部分。
print *, "=== Query Result==="
print '(a, i0, 2x, i0)', 'tuples, fields: ', PQntuples(res), PQnfields(res)
print *, "=== Available database names ==="
do i = 0, PQntuples(res)-1
print *, PQgetvalue(res, i, 0)
end do
首先调用函数PQntuples和PQnfields,显示res对象的结果行数和列数。然后,根据行数循环,使用函数PQgetvalue提取第i行第0列的数据并显示出来。请注意,这里使用了C语言规则中的基于0的索引。
最后调用PQclear函数和PQfinish函数释放结果对象,并结束连接。
call PQclear(res)
call PQfinish(conn)
执行结果
按照这样的方式编写的程序将在执行时,在终端上输出如下所示的显示,就像在上面执行的一样。
=== Query Result===
tuples, fields: 3 1
=== Available database names ===
postgres
template1
template0
如果在这里至少显示出(在PostgreSQL服务器的默认配置下)postgres、template1、template0这三个数据库名称,那么就表示成功了。
最后
我们发布了一个用于访问PostgreSQL官方库libpq的Fortran模块Libpq-Fortran,并介绍了如何首次使用它。
由于Libpq-Fortran只是直接封装了libpq,因此在实际管理应用程序数据时需要有一定的SQL和数据库设计知识。特别是关于使用PostgreSQL数据库来实现数值计算数据保存的方法,因篇幅过长,本文将在另一篇文章中详细介绍。
感谢
参考文献1は@cure_honeyさんに教えていただきました。ありがとうございます。
このライブラリを作ったのは、2023年9月10日のモダンFortran勉強会.forでのディスカッションがきっかけの一つです。
引用文献
- SQLite -使用现代Fortran进行编程,由Philipp Engel撰写
補充
以下是demo.f90程序的整体内容。
program demo
use :: libpq
use, intrinsic :: iso_c_binding
use, intrinsic :: iso_fortran_env, only:stdout=>output_unit, stdin=>input_unit
type(c_ptr) :: conn, res
character(:, kind=c_char), allocatable :: sql, conninfo
integer :: i, port
character(256) :: str, host, dbname, user, password
print *, '=== INPUT Database Information ==='
print *, '=== type "q" for quit ==='
write (stdout, '(a)', advance='no') "Hostname: "
read (stdin, *) host
if (host == 'q') stop
port = 5432
write (stdout, '(a)', advance='no') "dbname: "
read (stdin, *) dbname
if (dbname == 'q') stop
write (stdout, '(a)', advance='no') "user: "
read (stdin, *) user
if (user == 'q') stop
write (stdout, '(a)', advance='no') "password: "
read (stdin, *) password
if (password == 'q') stop
print *, "=== END INPUT ==="
write(str, '(a, i0, a)') &
"host="//trim(adjustl(host))// &
" port=", port, &
" dbname="//trim(adjustl(dbname))// &
" user="//trim(adjustl(user))// &
" password="//trim(adjustl(password))
conninfo = str
conn = PQconnectdb(conninfo)
if (PQstatus(conn) /= 0) then
print *, PQerrorMessage(conn)
error stop
end if
! The query to retrieve the names of databases within a database cluster is:
sql = "select datname from pg_database;"
res = PQexec(conn, sql)
if (PQstatus(conn) /= 0 ) then
print *, PQerrorMessage(conn)
end if
print *, "=== Query Result==="
print '(a, i0, 2x, i0)', 'tuples, fields: ', PQntuples(res), PQnfields(res)
print *, "=== Available database names ==="
do i = 0, PQntuples(res)-1
print *, PQgetvalue(res, i, 0)
end do
call PQclear(res)
call PQfinish(conn)
end program demo