rakulang, dartlang, nimlang, golang, rustlang, lang lang no see

Psql

焉知非鱼

Psql

Raku 与 PostgreSQL 的连接性一览

在我看来,Raku 是一门伟大的语言,我每天都在使用它,而且越来越多。我可以说它将取代我的 Perl 脚本。

Raku 有一个广泛的模块库,当然包括数据库连接,这反过来又包括连接 PostgreSQL 的功能。 在这篇简单的文章中,我将快速演示如何使用 Raku 的一段代码来完成许多比数据库应用程序还琐碎的任务。 脚本是以增量的方式呈现的,所以连接数据库部分必须始终作为脚本的前言。

DB::Pg 模块在某种程度上与 Perl 5 的 DBD::Pg 很相似,所以很多概念和方法名都会让人想起后者。

安装方法 #

可以使用 zef 来安装 DB::Pg 模块。

% zef install DB::Pg

根据你的系统速度和已经安装的库,可能需要几分钟的时间。

如果你要使用 LISTEN/NOTIFY,你需要同时安装 epoLl.NET 和 EPOLl.NET。

% zef install epoll

连接到数据库 #

现在可以使用 DB::Pg 模块连接到数据库。例如,一个简单的脚本可以接受命令行上的所有参数(清晰的文本!),可以是:

#!raku

use DB::Pg;

sub MAIN( Str :$host = 'miguel',
          Str :$username = 'luca',
          Str :$password = 'secet',
          Str :$database = 'testdb' ) {

    "Connecting $username @ $host/$database".say;

    my $connection = DB::Pg.new: conninfo => "host=$host user=$username password=$password dbname=$database";

如你所见,DB::Pg模块接受一个 conninfo 字符串。

读取查询和结果 #

.query 方法允许向数据库发出读取查询。结果是一个 Result 类对象,它可以通过不同的方法来使用,最著名的是 .hash.arrays,它们返回一连串的 hash 或 arrays,从查询中提取的每一行都有一个 .rows.column 等特殊方法分别提供了查询返回的行数和结果集的列名。

举个例子,这里是一个简单的查询。

my $query = 'SELECT current_role, current_time';
my $results = $connection.query: $query;

say "The query { $query } returned { $results.rows } rows with columns: { $results.columns.join( ', ' ) }";
for $results.hashes -> $row {
    for $row.kv -> $column, $value {
        say "Column $column = $value";
    }
}

上面这段代码提供了一个类似于下面的输出。

查询 SELECT current_role, current_time 返回1行,列数为: current_role, current_time。

Column current_role = luca
Column current_time = 14:48:47.147983+02

光标 #

默认情况下,.query 方法将从查询中获取所有的行,这对于较大的数据集来说是一个问题。可以使用 .cursor 方法,它可以接受可选的批量大小(默认为1000个元组),并可选地接受将结果获取为哈希序列的指定器。

作为一个简单的例子。

for $connection.cursor( 'select * from raku', fetch => 2, :hash ) -> %row {
    say "====================";
    for %row.kv -> $column, $value {
        say "Column [ $column ] = $value";
    }
    say "====================";
}

产生和输出像这样的东西。

====================
Column [ pk ] = 2
Column [ t ] = This is value 0
====================
====================
Column [ pk ] = 3
Column [ t ] = This is value 1
====================
====================
Column [ t ] = This is value 2
Column [ pk ] = 4
====================
====================
Column [ pk ] = 5
Column [ t ] = This is value 3
====================
...

撰写声明 #

编写语句可以通过 .execute 方法来执行,如:

$connection.execute: q< insert into raku( t ) values( 'Hello World' )>;

交易和编制报表 #

为了处理事务,你需要访问被"屏蔽"到 DB::Pg 主对象中的数据库处理程序。数据库对象像往常一样提供了 .begin.rollback.commit等方法。

此外,还可以使用 .prepare 方法来获得一个已准备好的语句,该语句可以被缓存并用于循环和重复性任务中。值得注意的是,.prepare 方法使用了 $1$2 等参数占位符,当语句接受单个值时,必须在 .execute 中不指定索引。

举个例子

my $database-handler = $connection.db;
my $statement = $database-handler.prepare: 'insert into raku( t ) values( $1 )';

$database-handler.begin;
$statement.execute( "This is value $_" )  for 0 .. 10;
$database-handler.commit;
$database-handler.finish;

上述循环相当于一个SQL事务,如:

BEGIN;
INSERT INTO raku( t ) VALUES ('This is value 0' );
INSERT INTO raku( t ) VALUES ('This is value 1' );
INSERT INTO raku( t ) VALUES ('This is value 2' );
...
INSERT INTO raku( t ) VALUES ('This is value 10' );
COMMIT;

.finish 方法是必需的,因为 DB::Pg 处理缓存。请注意,.commit.rollback 方法是流畅的,并返回一个对象实例,这样你就可以调用 .commit.finish

数据库与连接 #

缓存的处理方式是,当发出一个查询时,会打开一个新的连接并使用。一旦工作完成,连接就会返回到内部池中。DB::Pg::Database 对象做的工作和 DB::Pg 的一样,不同的是它不会自动将连接返回到池中,所以需要自己进行 . 完成。

因此,你可以在两个对象上使用相同的 .query.execute 方法,但 DB::Pg 会自动将连接返回到内部池中,而数据库对象则允许你对何时将连接返回到池中进行细粒度的控制。

复制 #

PostgreSQL 提供了特殊的 COPY 命令,可以用来复制从和进入。有一个方法 .copy-in 可以执行 COPY FROM,而 COPY TO 可以在迭代循环中使用。

my $file = '/tmp/raku.csv'.IO.open: :w;
for $connection.query: 'COPY raku TO stdout (FORMAT CSV)'  -> $row {
    $file.print: $row;
}

以上将 CSV 结果导出到文本文件上。 如果要读回数据,可以发出 .copy-in 方法,但首先需要发出 SQL COPY。工作流程是

issue a COPY FROM STDIN;
use .copy-data to slurp all the data;
use .copy-end to notify the database that the COPY is concluded.

对.copy-end的需求是一个建议:可以在一次运行中发出不同的.copy-data,例如从不同文件中导入数据。

$database-handler = $connection.db;
$database-handler.query: 'COPY raku FROM STDIN (FORMAT CSV)';
$database-handler.copy-data:  '/tmp/raku1.csv'.IO.slurp;
$database-handler.copy-data:  '/tmp/raku2.csv'.IO.slurp;
$database-handler.copy-end;

转换器 #

可以指定转换器,即处理进出数据库的值的特殊角色;这让我想起了 DBI::Class 的 inflatedeflate 选项。 第一步是在 DB::Pg 中给转换器实例添加一个角色,这样的实例必须。

  • 增加一个新的类型转换方法。
  • 增加一个转换方法来处理类型字符串化的值,并返回新值(在任何 Raku 实例中)。

作为一个例子,下面将一个文本 PostgreSQL 类型转换为一个 Str Raku 对象,并在其内容上进行反转。

$connection.converter does role fluca-converter
{
    submethod BUILD { self.add-type( text => Str ) }
    multi method convert( Str:U, Str:D $value) {
        $value.flip.uc;
    }

}

.say for $connection.query( 'select * from raku' ).arrays;

产生类似于的输出。

[442 DLROW OLLEH]
[454 DLROW OLLEH]
[466 DLROW OLLEH]

其中字符串 Hello World 被翻转。

listen 和 notify #

DB::Pg也可以处理LISTEN和NOTIFY,它们能够与Raku的react动态功能进行交互。 首先,创建一个简单的机制来通知一些事件。

testdb=> create or replace rule r_raku_insert 
         as on insert to raku 
         do also 
         SELECT pg_notify( 'insert_event', 'INSERTING ROW(S)' );
CREATE RULE

testdb=> create or replace rule r_raku_delete
         as on delete to raku 
         do also 
         SELECT pg_notify( 'delete_event', 'DELETING ROW(S)' );
CREATE RULE

现在,可以创建一个等待传入事件的 Raku 脚本。

react {
    whenever $connection.listen( 'delete_event' ) { .say; }
    whenever $connection.listen( 'insert_event' ) { .say; }
}

目的是,每次发出一个事件,.listen 都会将消息有效载荷传递给 react 代码块。因此,发出一些 DELETEINSERT 会导致输出。

DELETING ROW(S)
INSERTING ROW(S)
INSERTING ROW(S)

可以通过 .unlisten 方法停止监听反应块。也可以通过 .notify 方法发出事件。

总结 #

DB::Pg 是 PostgreSQL 的一个很好的驱动程序,它允许 Raku 直接在语言中利用很多功能。

文章 A glance at Raku connectivity towards PostgreSQL 已经由 Luca Ferrari 发布在博客上。

原文链接: https://fluca1978.github.io/2021/03/29/RakuPostgreSQL.html