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

Deno でのデータベースアクセスを試したので、記録を残しておきます。
試すのはこれら。

  • deno_mysql
  • dso
  • DenoDB

それでは試していきます。

参考

環境準備

dene の実行環境は、docker で用意する。

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
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
privileged: true
entrypoint:
- /sbin/init
ports:
- "8080:8080"
volumes:
- .:/usr/src/app:cached
tty: true
db:
image: mysql:5.7
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
ports:
- "${MYSQL_PORT:-3306}:3306"
volumes:
- ./db/mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-}
dockerfile
1
2
3
4
5
6
7
8
9
10
FROM denoland/deno:centos

RUN yum update -y && \
yum install -y which systemd-sysv crontabs && \
yum install -y mysql-devel mysql

RUN mkdir /usr/src/app
WORKDIR /usr/src/app

EXPOSE 8080

deno_mysql

deno_mysql は、ORM ではなくてデータベースドライバー。
Active Record を触るのが日常になった今、なかなかデータベースドライバーを直接触ることが少なくなりました。

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
import { Client } from "https://deno.land/x/mysql/mod.ts";

// ロギング
import { configLogger } from "https://deno.land/x/mysql/mod.ts";
await configLogger({ enable: false });

// User 型定義
interface User {
id: number;
name: string;
created_at: string;
}

// Item 型定義
interface Item {
id: number;
user_id: number;
name: string;
created_at: string;
}

// 接続パラメータ
const connectionParam = {
hostname: "db",
username: "ユーザー名",
password: "パスワード",
db: "testdb",

};

// クライアント作成
const client = await new Client().connect(connectionParam);

// データベース作成
await client.execute(`CREATE DATABASE IF NOT EXISTS ${connectionParam.db}`);

// テーブル作成
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(100) NOT NULL,
created_at timestamp not null default current_timestamp,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);

// データ登録
const resultInsert = await client.execute(`Insert Into users(name) values(?)`, [
"username",
]);
console.log(resultInsert);
const targetId = resultInsert.lastInsertId;

// データ更新
let resultUpdate = await client.execute(
`update users set ?? = ? where ?? = ?`,
["name", "username_1", "id", targetId]
);
console.log(resultUpdate);

// データ削除
let result = await client.execute(`delete from users where ?? = ?`, [
"id",
targetId,
]);
console.log(result);

// データ検索
const users: User[] = await client.query(`select * from users`);
console.log(users);

// データ検索
const resultSelect = await client.execute(`select * from users`);
console.log(resultSelect);

// トランザクション

// テーブル作成
await client.execute(`
CREATE TABLE IF NOT EXISTS items (
id int(11) NOT NULL AUTO_INCREMENT,
user_id int(11) NOT NULL,
name varchar(100) NOT NULL,
created_at timestamp not null default current_timestamp,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);

// User に関連付けされた Itemを登録
const resultTran = await client.transaction(async (conn) => {
const result1 = await conn.execute(`Insert Into users(name) values(?)`, [
"username",
]);
console.log(result1);
const result2 = await conn.execute(
`Insert Into items(user_id, name) values(?, ?)`,
[result1.lastInsertId, "itemname"]
);
return { result1, result2 };
});

console.log(resultTran);

users.map((user) => {
console.log(user.id);
});

// クライアントの切断
await client.close();

昔、Promise-mysql とか Node MySQL 2 触っていたころのことを思い出しました。

dso - Deno Simple Orm -

先に試した、deno_mysql ベースの ORM だそう。
試してみたところ、deps.ts 内での std ライブラリのバージョン指定がどうも違うらしく、動作できず。
こういうの修正ってどうやって試したらいいんだろう。?

1
2
3
4
5
ソースコード
"https://deno.land/std@v0.51.0/testing/asserts.ts";

おそらく正しくは
"https://deno.land/std@0.51.0/testing/asserts.ts";

プルリク出せるかしら。

DenoDB

続けて DenoDB。
こちらは Deno 向け ORM.
とりあえず基本操作。

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
import {
DataTypes,
Database,
Model,
MySQLConnector,
} from "https://deno.land/x/denodb@v1.0.40/mod.ts";

// コネクションの作成
const connection = new MySQLConnector({
host: "db",
username: "ユーザー名",
password: "パスワード",
database: "testdb",
});

// DB 接続
const db = new Database(connection);

// モデル定義 テーブル名も含む形
class User extends Model {
static table = "users";

static fields = {
id: { primaryKey: true, autoIncrement: true },
name: DataTypes.STRING,
};

static defaults = {
name: "default_name",
};

static timestamps = true;
}

// DB と、モデル定義を連携
db.link([User]);

// DB にモデル定義を同期する{drop: true}を入れると、データベースとモデルに不整合があると一旦dropされるこれはやや怖い
await db.sync({ drop: true });
// おそらく普段使うのはこちら
// await db.sync();

// データ登録
await User.create({});
// User { id: 1, name: "default_name", createdAt: 2021-06-30T15:40:49.000Z, updatedAt: 2021-06-30T15:40:49.000Z } が作成される

await User.create({
name: "user_A",
});

const user1 = new User();
user1.name = "user_B";
await user1.save();

// 複数まとめて作成もできる
await User.create([
{
name: "user_C",
},
{
name: "user_D",
},
]);

// データ更新
const user2 = await User.where({ id: 4 });
await user2.update({ name: "user_CCCC" });

// select
// .get()で、クエリを実行する
console.log(
await User.select("id").where("id", ">", "3").get()
//await User.select("id").where("id", ">", "3").get().toDescription() <= v1.0.40 で .toDescription() が不要になっていた
);
// =>[ User { id: 4 }, User { id: 5 } ]
// console.log(await User.select("id").where('id','>','3').all()); でも同じ結果になる。
// ドキュメントでも「読みやすくなる」というのが違いとなっているのが面白いところ。

// 個数のカウント
console.log(await User.where("id", ">", "3").count());
// => 2

// 他にもメソッドは多数。
// https://eveningkid.com/denodb-docs/docs/api/model-methods

// データ削除[個別1]
await User.deleteById("1");

// データ削除[個別2]
const user3 = await User.where({ id: 2 });
await user3.delete();
// await User.find('2').delete(); でもOK
// 内部では、.deleteById が呼び出されている。

// データ削除[まとめて消す]
// await User.all().delete() みたいなことができなかった。他にやり方はあるかも
const user4 = await User.all();
const deleteQuerys = user4.map(async (u) =>
!u.id ? "" : await User.deleteById(u.id.toString())
);
await Promise.all(deleteQuerys);

// 接続をクローズ
await db.close();

イイ感じに使えるが、toSql()的なメソッドが公開されていないところが少々残念。
private では持っているようなので、できれば公開されてほしい。

一旦は、データベース側で実行クエリをロギングして、おけばイイかな?という見込み。
MySQL に投げられたすべての SQL クエリをロギングする

リレーション

続けて、リレーションとトランザクションは以下のようになります。
ドキュメントに書いているとおりだとエラーになること確認したので、注意が必要でした。

外部キー

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
import {
Database,
DataTypes,
Model,
MySQLConnector,
Relationships,
} from "https://deno.land/x/denodb@v1.0.40/mod.ts";

// コネクションの作成
const connection = new MySQLConnector({
host: "db",
username: "ユーザー名",
password: "パスワード",
database: "testdb",
});

// DB 接続
const db = new Database(connection);

// モデル定義 テーブル名も含む形
class User extends Model {
static table = "users";

static fields = {
// type: DataTypes.INTEGER にしておかないと、table[relationshipPKType](fieldOptions.name);というエラーになった。
id: { primaryKey: true, autoIncrement: true, type: DataTypes.INTEGER },
Name: DataTypes.STRING,
};

static defaults = {
name: "default_name",
};

static timestamps = true;
}

class Item extends Model {
static table = "items";

static fields = {
id: { primaryKey: true, autoIncrement: true, type: DataTypes.INTEGER },
//userId: DataTypes.STRING,
name: DataTypes.STRING,
};

static defaults = {
name: "default_item_name",
};

// クエリメソッドの追加
static user() {
return this.hasOne(User);
}

static timestamps = true;
}

Relationships.belongsTo(Item, User);

// DB と、モデル定義を連携
db.link([User, Item]);

await db.sync({ drop: true });
// await db.sync();

const { lastInsertId } = await User.create({});

if (!!lastInsertId) {
await Item.create({ userId: lastInsertId as number });

console.log(await User.all());
// => [ User { id: 1, Name: "default_name", createdAt: 2021-07-01T09:35:28.000Z, updatedAt: 2021-07-01T09:35:28.000Z }]

console.log(await Item.all());
// => [Item { id: 1, name: "default_item_name", userId: 1, createdAt: 2021-07-01T09:35:28.000Z, updatedAt: 2021-07-01T09:35:28.000Z }]

console.log(await Item.where("id", 1).user());
// => [ User { id: 1, Name: "default_name", createdAt: 2021-07-01T09:35:28.000Z, updatedAt: 2021-07-01T09:35:28.000Z }]

// console.log(await Item.find(1).user()) はできない!
// where と find で返却するオブジェクトが違うため
}

Item -> User は追えるが、逆をたどることはできない。

一対多

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
import {
Database,
DataTypes,
Model,
MySQLConnector,
Relationships,
} from "https://deno.land/x/denodb@v1.0.40/mod.ts";

// コネクションの作成
const connection = new MySQLConnector({
host: "db",
username: "ユーザー名",
password: "パスワード",
database: "testdb",
});

// DB 接続
const db = new Database(connection);

// モデル定義 テーブル名も含む形
class User extends Model {
static table = "users";

static fields = {
// type: DataTypes.INTEGER にしておかないと、table[relationshipPKType](fieldOptions.name);というエラーになった。
id: { primaryKey: true, autoIncrement: true, type: DataTypes.INTEGER },
Name: DataTypes.STRING,
};

static defaults = {
name: "default_name",
};

static items() {
return this.hasMany(Item);
}

static timestamps = true;
}

class Item extends Model {
static table = "items";

static fields = {
id: { primaryKey: true, autoIncrement: true, type: DataTypes.INTEGER },
//userId: DataTypes.STRING,
name: DataTypes.STRING,
};

static defaults = {
name: "default_item_name",
};

// クエリメソッドの追加
static user() {
return this.hasOne(User);
}

static timestamps = true;
}

Relationships.belongsTo(Item, User);

// DB と、モデル定義を連携
db.link([User, Item]);

await db.sync({ drop: true });
// await db.sync();

const { lastInsertId } = await User.create({});

if (!!lastInsertId) {
// Userに関連したItemを3件登録
await Item.create([
{ userId: lastInsertId as number },
{ userId: lastInsertId as number },
{ userId: lastInsertId as number },
]);

console.log(await User.all());
// => [ User { id: 1, Name: "default_name", createdAt: 2021-07-01T09:35:28.000Z, updatedAt: 2021-07-01T09:35:28.000Z }]

console.log(await Item.all());
// => [Item { id: 1, name: "default_item_name", userId: 1, createdAt: 2021-07-01T09:35:28.000Z, updatedAt: 2021-07-01T09:35:28.000Z }]

console.log(await Item.where("id", 1).user());
// => User { id: 1, Name: "default_name", createdAt: 2021-07-01T09:35:28.000Z, updatedAt: 2021-07-01T09:35:28.000Z }

console.log(await User.where("id", 1).items());
// => 3件データを取得できる
}

// 接続をクローズ
await db.close();

ActiveRecordでイメージするような、データの取得ができました。
hasManyなリレーションだけ上の通り記載しましたが、もちろん1対1 多対多もあります。

トランザクション

最後にトランザクション。
残念ながら、__このコードは動きません__。

error: Module evaluation is still pending but there are no pending ops or dynamic imports. This situation is often caused by unresolved promise.

となります。

更新:v1.0.40 ではエラーにならなくなったが、Promiseの結果が返ってこないという状況になっている。

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
import {
Database,
DataTypes,
Model,
MySQLConnector,
Relationships,
} from "https://deno.land/x/denodb@v1.0.40/mod.ts";

// コネクションの作成
const connection = new MySQLConnector({
host: "db",
username: "ユーザー名",
password: "パスワード",
database: "testdb",
});

// DB 接続
const db = new Database(connection);

// モデル定義 テーブル名も含む形
class User extends Model {
static table = "users";

static fields = {
id: { primaryKey: true, autoIncrement: true, type: DataTypes.INTEGER },
Name: DataTypes.STRING,
};

static defaults = {
name: "default_name",
};

static items() {
return this.hasMany(Item);
}

static timestamps = true;
}

class Item extends Model {
static table = "items";

static fields = {
id: { primaryKey: true, autoIncrement: true, type: DataTypes.INTEGER },
//userId: DataTypes.STRING,
name: DataTypes.STRING,
};

static defaults = {
name: "default_item_name",
};

static user() {
return this.hasOne(User);
}

static timestamps = true;
}

Relationships.belongsTo(Item, User);

// DB と、モデル定義を連携
db.link([User, Item]);

await db.sync({ drop: true });
// await db.sync();

await db.transaction(async () => {
const { lastInsertId } = await User.create({});

if (!lastInsertId) return

await Item.create([
{ userId: lastInsertId as number },
{ userId: lastInsertId as number },
{ userId: lastInsertId as number },
]);
});

// 接続をクローズ
await db.close();


DenoDB かなりイイじゃないか!と感じていたのも束の間、トランザクションが上手く動かなかったり。
「deno_mysql使うのが結局素直なのでは?」というところが、所感でした。
「Deno 使いの方々は、DBのアクセスに何を使っているのか?」というところが興味あるところです。

ではでは。