Deno で試すデータベースアクセス(PostgreSQL編)

Deno.inc が、Supabase とパートナーシップを締結。 Supabase Functions という Deno Deploy
ベースのエッジワーカーが発表されていました。

Supabase は、データベースが提供されるんだけど MYSQL ではなく、PostgreSQL だそうな。 PostgreSQL
は、利用経験が無いので一旦こちらを試すことにした。 (PostgreSQL 記事書きながら 8 年位前に使ったことあったのを思い出した。)

参考

環境構築

いつものように docker compose で環境の用意をします。 普段は、denoland/deno:centos を使っていました。 最近は
設定をしないと yum が動かなかったりと都合悪いことが増えてきました。 というわけで今回は、denoland/deno(debian) を使用します。

dockerfile
1
2
3
4
FROM denoland/deno:1.20.4

RUN mkdir /usr/src/app
WORKDIR /usr/src/app
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
privileged: true
entrypoint:
- /bin/sh
volumes:
- .:/usr/src/app:cached
tty: true

postgres:
image: postgres:latest
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER:-user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
PGPASSWORD: ${PGPASSWORD:-pgpassword}
POSTGRES_DB: ${APP_DB:-app}
TZ: "Asia/Tokyo"
ports:
- 5432:5432
volumes:
- ./postgres:/var/lib/postgresql/data

pgadmin:
image: dpage/pgadmin4
restart: always
ports:
- 8080:80
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-test@test.com}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-password}
volumes:
- ./pgadmin:/var/lib/pgadmin
depends_on:
- postgres
.env
1
2
3
4
5
6
POSTGRES_USER=hogehoge
POSTGRES_PASSWORD=hogehoge
PGPASSWORD=hogehoge
POSTGRES_DB=hogehoge
PGADMIN_DEFAULT_EMAIL=hogehoge
PGADMIN_DEFAULT_PASSWORD=hogehoge

以上を用意して以下を操作。

1
2
3
4
5
docker-compose up -d
docker-compose exec app bash

deno -V
deno 1.20.4

pgadmin 接続

普段は SQL クライアントとして HeidiSQL を使っているのですが、こちらから PostgreSQL への接続はできませんでした。
そのため、クライアントとして pgadmin を用意してあります。 localhost:8080 にアクセスすると、pgadmin へログインできます。

「新しいサーバーかを追加」から設定を入れ、pgadmin -> postgres の接続設定を投入します。

ざっとデータを見るのは楽でした。

実装

今回は、PostgreSQL のクライアントとして deno.land/x の検索結果から 星の比較的多いもの 2 つ試します。

postgresjs

README に TypeScript 対応の記載はあるものの、上手いこといかないので JavaScript で記述。

sample1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import "https://deno.land/std@0.133.0/dotenv/load.ts";
import postgres from "https://deno.land/x/postgresjs/mod.js";

const sql = postgres({
host: "postgres",
port: 5432,
database: Deno.env.get("POSTGRES_DB"),
username: Deno.env.get("POSTGRES_USER"),
password: Deno.env.get("POSTGRES_PASSWORD"),
});

async function createUserTable() {
await sql`
CREATE TABLE IF NOT EXISTS users (
id serial primary key,
name text,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`;
}

async function createUserFunction() {
await sql`DROP FUNCTION IF EXISTS set_update_time() CASCADE;`;
await sql`
CREATE FUNCTION set_update_time() RETURNS trigger AS '
begin
new.updated_at := ''now'';
return new;
end;
' LANGUAGE plpgsql;`;
await sql`CREATE TRIGGER update_trigger BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE set_update_time();`;
}

async function getUsers() {
return await sql`
select
id, name, created_at, updated_at
from
users
order by
id
`;
}

async function insertUser(name) {
return await sql`
insert into users
(name)
values
(${name})
returning name
`;
}

async function updateUser(id, { name }) {
return await sql`
update
users
set
name = ${name}
where
id = ${id}
`;
}

// テーブル作成
await createUserTable();

// 関数を設定
await createUserFunction();

// データを挿入
await insertUser("AAA1");
await insertUser("AAA2");

// データ更新
await updateUser(1, { name: "BBB1" });

// データ取得
console.log(await getUsers());

// 接続切断
await sql.end();

実行すると以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
deno run --allow-net --allow-env --allow-read --unstable sample1.js
[
{
id: 1,
name: "BBB1",
created_at: 2022-04-03T18:02:29.743Z,
updated_at: 2022-04-03T18:02:29.749Z
},
{
id: 2,
name: "AAA2",
created_at: 2022-04-03T18:02:29.747Z,
updated_at: 2022-04-03T18:02:29.747Z
}
]

実行には、–unstable が必要。

postgres.js の提供している 関数見てみると気持ち悪い点があります。関数に()が無い。
タグ付きテンプレート関数というものだそうです。これがエスケープもそうです。 というわけでエスケープは気にせず ${} を使って文字列を埋め込んでよい。

とりあえず、データの取得ができました。

deno-postgres

次は、deno 向け実装の deno-postgres を試します。

sample2.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import "https://deno.land/std@0.133.0/dotenv/load.ts";
import { Client } from "https://deno.land/x/postgres/mod.ts";

const client = new Client({
hostname: "postgres",
port: 5432,
database: Deno.env.get("POSTGRES_DB"),
user: Deno.env.get("POSTGRES_USER"),
password: Deno.env.get("POSTGRES_PASSWORD"),
});

await client.connect();

interface User {
id: number;
name: string;
created_at: Date;
updated_at: Date;
}

async function createUserTable() {
await client.queryObject({
text: `
CREATE TABLE IF NOT EXISTS users (
id serial primary key,
name text,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`,
});
}

async function createUserFunction() {
await client.queryObject({
text: `
DROP FUNCTION IF EXISTS set_update_time() CASCADE;
CREATE FUNCTION set_update_time() RETURNS trigger AS '
begin
new.updated_at := ''now'';
return new;
end;
' LANGUAGE plpgsql;
CREATE TRIGGER update_trigger BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE set_update_time();
`,
});
}

async function getUsers(): Promise<any[][]> {
const users = await client.queryArray({
text: `
select
id,
name,
created_at,
updated_at
from
users
order by
id
`,
});
return users.rows;
}

async function getUsersObject(): Promise<User[]> {
const users =
await client.queryObject<
User >
({
text: `
select
id,
name,
created_at,
updated_at
from users
order by id
`,
fields: ["id", "name", "created_at", "updated_at"],
});
return users.rows;
}

async function insertUser({ name }: { name: string }) {
const users = await client.queryObject({
text: `
insert into users
(name)
values
($1)
`,
args: [name],
fields: ["name"],
});
return users;
}

async function updateUser(id: number, { name }: { name: string }) {
const users = await client.queryObject({
text: `
update
users
set
name = $1
where
id = $2
`,
args: [name, id],
fields: ["name"],
});
return users;
}

// テーブル作成
await createUserTable();

// 関数を設定
await createUserFunction();

// データを挿入
await insertUser({ name: "AAA1" });
await insertUser({ name: "AAA2" });

// データ更新
await updateUser(1, { name: "BBB1" });

// データ取得
console.log(await getUsers());
console.log(await getUsersObject());

// 接続切断
await client.end();

実行すると以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deno run --allow-net --allow-env --allow-read sample2.ts
[
[ 1, "BBB1", 2022-04-03T18:31:16.654Z, 2022-04-03T18:31:16.662Z ],
[ 2, "AAA2", 2022-04-03T18:31:16.660Z, 2022-04-03T18:31:16.660Z ]
]
[
{
id: 1,
name: "BBB1",
created_at: 2022-04-03T18:31:16.654Z,
updated_at: 2022-04-03T18:31:16.662Z
},
{
id: 2,
name: "AAA2",
created_at: 2022-04-03T18:31:16.660Z,
updated_at: 2022-04-03T18:31:16.660Z
}
]

面白いのは、select 結果を 2 種の方法で取り出せること。 ナイスポイントは、クエリの取得結果に型が付くこと。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function getUsersObject(): Promise<User[]> {
const users = await client.queryObject<User>({
text: `
select
id,
name,
created_at,
updated_at
from users
order by id
`,
fields: ["id", "name", "created_at", "updated_at"],
});
return users.rows;
}

例えば、getUsersObject()から型定義を外した以下のものを用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function getUsersObject() {
const users = await client.queryObject({
text: `
select
id,
name,
created_at,
updated_at
from users
order by id
`,
fields: ["id", "name", "created_at", "updated_at"],
});
return users.rows;
}

そして取得結果を扱う以下のコードを用意します。

1
2
3
4
5
// この実装自体には、何も意味は無い
const users = await getUsersObject();
const ids = users.map((u) => {
return u.id * u.id;
});

実行すると型チェックでエラーとなります。

1
2
3
4
5
6
7
8
9
error: TS2571 [ERROR]: Object is of type 'unknown'.
return u.id * u.id;
^
at file:///usr/src/app/deno-postgres.ts:134:10

TS2571 [ERROR]: Object is of type 'unknown'.
return u.id * u.id;
^
at file:///usr/src/app/deno-postgres.ts:134:17

今回 2 つ PostgreSQL のクライアントを試してみました。 使っていく場合には、deno-postgres の方を選択したいかなと感じました。

ではでは。