Truso の動きが活発。
以前は CLI でしか、作ったテーブルを確認できなかったのが、管理画面も増えた。
無料枠も激増した。
そんな Turso は、Kysely をサードパーティの開発者ツールとして公式に記載している。
ここではクエリビルダーとして記載されているが、マイグレーションや ORM 寄りの機能も持っている。
(TypeScript 用のタイプセーフな SQLクエリ ビルダーということで、推論も優秀な様子。)
ということで、Turso(libsql) x Kysely を試してみる。
参考
実装
Kysely を使用したクエリ発行、マイグレーションを概ね Kysely の Installation にしたがって進める。
import
deno.json での import-map は次のように定義。
deno.json1 2 3 4 5 6 7 8 9 10 11
| { "tasks": { "dev": "deno run --watch main.ts" }, "imports": { "kysely": "npm:kysely@^0.25.0", "@libsql/kysely-libsql": "npm:@libsql/kysely-libsql", "std/": "https://deno.land/std@0.203.0/", "cliffy": "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts" } }
|
テーブル/型定義
./db/types.ts を作成。
/db/types.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { ColumnType, Generated, } from "kysely";
export interface Database { user: userTable; food: FoodTable; }
export interface userTable { id: Generated<number>; name: string | null; created_at: ColumnType<Date, string | undefined, never>; }
export interface FoodTable { id: Generated<number>; user_id: number name: string; created_at: ColumnType<Date, string | undefined, never>; }
|
コネクション作成
DBに接続をするがこれが簡単。
/db/db.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { Database } from "./types.ts"; import { Kysely } from "kysely"; import { LibsqlDialect } from "@libsql/kysely-libsql";
const dialect = new LibsqlDialect({ url: Deno.env.get("TURSO_DATABASE_URL"), authToken: Deno.env.get("TURSO_AUTH_TOKEN"), });
export const client = new Kysely<Database>({ dialect, log(event) { if (event.level === "query") { console.log("query:", event.query.sql); console.log("parameters:", event.query.parameters); } }, });
|
urlや、トークンなどは接続先の turso のものを環境変数経由で適用する。
一旦接続確認。
クライアントが作成できたので、一旦クエリ実行してみる。
次のようにする。
main.ts1 2 3 4 5 6 7 8 9 10
| import "https://deno.land/std@0.203.0/dotenv/load.ts"; import { client } from "./db/client.ts";
const users = await client.selectFrom("user").selectAll().execute(); console.log(users);
users.map((u) => { console.log(u.id); console.log(u.name); });
|
まだマイグレーションはしていないので、一旦手動で turso のDBにテーブル作成しておく実行しておく。
1 2
| create table user(id INTEGER PRIMARY KEY, name text); insert into user(name) values('田中');
|
1 2 3 4 5 6
| $ deno run --allow-net --allow-read --allow-env ./main.ts query: select * from "user" parameters: [] [ { id: 1, name: "田中" } ] 1 田中
|
1 2 行目は、クライアント作成時の log オプションが効いている。
1 2 3 4 5 6 7 8 9
| export const client = new Kysely<Database>({ dialect, log(event) { if (event.level === "query") { console.log("query:", event.query.sql); console.log("parameters:", event.query.parameters); } }, });
|
3 行目以降は、main.ts の記述された出力結果が出ている。
型推論がよく効いていて client.selectFrom("user").selectAll().execute()
の実行結果にもよく出ている。
client.selectFrom("user").selectAll().execute() の推論結果1 2 3 4 5
| (method) SelectQueryBuilder<Database, "user", { id: number; name: string | null; created_at: Date; }>.execute(): Promise<{ id: number; name: string | null; created_at: Date; }[]>
|
推論がよく効くことで候補もしっかりしており、実装も楽。
マイグレーションの実装
マイグレーションは、公式資料とともに、issueを参考に用意した。
https://github.com/kysely-org/kysely/issues/362
公式ドキュメントのサンプルでは、kysely が提供する FileMigrationProvider を使用している。
こちらが path と fs を求める。
これは、node:fs
node:path
で解決するが実行時エラーになる。
1 2 3 4 5 6 7 8
| import { promises as fs } from 'node:fs' import { path } from "node:path"
const provider = new FileMigrationProvider({ fs, path, migrationFolder: path.join(__dirname, './migrations'), }),
|
参考にした、issueでは、FileMigrationProvider の代替を独自実装している。
これをそのまま適用したが、上手く動作できず、次のようにした。
/db/file_migration_provider.ts1 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
| import { Migration, MigrationProvider } from "kysely"; import { join } from "std/path/mod.ts"; export class FileMigrationProviderOnDeno implements MigrationProvider { #folder: string;
constructor(folder: string) { this.#folder = folder; }
async getMigrations(): Promise<Record<string, Migration>> { const migrations: Record<string, Migration> = {}; let tmpPath = import.meta.resolve(this.#folder).split("file://")[1]; if (tmpPath.includes(":") && tmpPath.startsWith("/")) { tmpPath = tmpPath.split(/^\//)[1]; } const files = await Deno.readDir(tmpPath);
for await (const file of files) { console.log(file.name); console.log(join(import.meta.resolve(this.#folder), file.name)); migrations[file.name] = await import( join(import.meta.resolve(this.#folder), file.name) ); }
return migrations; } }
|
差異としては、import.meta
を使用して、パスの解決した。
windows 環境でも使用可能なように調整した。
windows 10、debian(docker) で動作確認。
CLI ツール化するに当たり、次のようにした。
今回は、cliffy を使ってみた。
deno.land/x で☆3つのCLIフレームワークである。
/db/migrate.ts1 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
| import "https://deno.land/std@0.203.0/dotenv/load.ts"; import { MigrationResult, Migrator } from "kysely"; import { client } from "./client.ts"; import { FileMigrationProviderOnDeno } from "./file_migration_provider.ts"; import { Command } from "cliffy";
const migrator = new Migrator({ db: client, provider: new FileMigrationProviderOnDeno("./migrations") });
const showReslut = async ( results: MigrationResult[] | undefined, error: unknown ) => { results?.forEach((it) => { if (it.status === "Success") { console.log(`migration "${it.migrationName}" was executed successfully`); } else if (it.status === "Error") { console.error(`failed to execute migration "${it.migrationName}"`); } });
if (error) { console.error("failed to run `migrateToLatest`"); console.error(error); } };
const doMigrate = async () => { const { results, error } = await migrator.migrateToLatest(); showReslut(results, error); };
const doRollback = async () => { const { results, error } = await migrator.migrateDown(); showReslut(results, error); };
const getDateTimeString = () => { const now = new Date(); const year = now.getFullYear(); const month = `${now.getMonth() + 1}`.padStart(2, "0"); const date = `${now.getDate()}`.padStart(2, "0"); const hour = `${now.getHours()}`.padStart(2, "0"); const minute = `${now.getMinutes()}`.padStart(2, "0"); const second = `${now.getSeconds()}`.padStart(2, "0"); return `${year}${month}${date}${hour}${minute}${second}`; };
const doCreateMigration = async (fileName: string) => { const kyselySkeletonText = `import { Kysely, sql } from "npm:kysely@^0.25.0";
export async function up(db: Kysely<any>): Promise<void> { }
export async function down(db: Kysely<any>): Promise<void> { } `;
const path = import.meta .resolve(`./migrations/${getDateTimeString()}${fileName}.ts`) .split("file:///")[1]; Deno.writeTextFileSync(path, kyselySkeletonText, { create: true }); console.info(`created migration file: ${path}`); };
const { cmd, options, args } = await new Command() .name("kysely migration tool") .command("migrate", "migrate to latest.") .action(async () => { console.log("migrate"); await doMigrate(); Deno.exit(0); }) .command("rollback", "rollback 1 migration.") .action(async () => { await doRollback(); Deno.exit(0); }) .command("create <name:string> [name:string]", "create migration.") .action((_options: any, source: string, _destination?: string) => { doCreateMigration(source); Deno.exit(0); }) .stopEarly() .parse(Deno.args);
cmd.showHelp();
|
ツールが用意できたので、マイグレーションを進める。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| $ deno run --allow-env --allow-read --allow-net
Usage: kysely migration tool
Options:
-h, --help - Show this help.
Commands:
migrate - migrate to latest. rollback - rollback 1 migration. create <name> [name] - create migration.
$ deno run --allow-env --allow-read --allow-write --allow-net ./db/migrate.ts create create_user created migration file: hogehoge/db/migrations/20231008035938create_user.ts
|
ソースを見ての通り、マイグレーションのテンプレートを書き込んでいる。
db/migrations/20231008035938create_user.ts1 2 3 4 5 6 7
| import { Kysely, sql } from "npm:kysely@^0.25.0";
export async function up(db: Kysely<any>): Promise<void> { }
export async function down(db: Kysely<any>): Promise<void> { }
|
これを次のように書き換える。
db/migrations/20231008035938create_user.ts(変更)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { Kysely, sql } from "npm:kysely@^0.25.0";
export async function up(db: Kysely<any>): Promise<void> { await db.schema .createTable("user") .addColumn("id", "integer", (col) => col.primaryKey()) .addColumn("name", "text", (col) => col.notNull()) .addColumn( "created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull(), ) .execute(); }
export async function down(db: Kysely<any>): Promise<void> { await db.schema.dropTable("user").execute(); }
|
続けてコマンドで、マイグレーション実行。
1 2
| $ deno run --allow-env --allow-read --allow-net ./db/migrate.ts migrate
|
turso 側のテーブルを表示すると次のようになっている。
1 2 3 4 5 6 7
| → .tables kysely_migration kysely_migration_lock user
→ select * from user; ID NAME CREATED AT
|
マイグレーションができた。
insert と select
先に実装した main.ts を書き換える。
CRUD すべて用意した。
main.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
| import "https://deno.land/std@0.203.0/dotenv/load.ts"; import { client } from "./db/client.ts";
console.log("\n[SELECT]"); console.log("select:",await client.selectFrom("user").selectAll().execute());
console.log("\n[INSERT]"); const insertResult = await client .insertInto("user") .values({ name: "山田", }) .returning(['id', 'name']) .executeTakeFirstOrThrow(); console.log(insertResult); console.log("select:", await client.selectFrom("user").selectAll().execute());
console.log("\n[UPDATE]"); const updateResult = await client .updateTable("user") .set({ name: "鈴木", }) .where("id", "=", insertResult.id) .returning(['id', 'name']) .execute(); console.log(updateResult); console.log("select:",await client.selectFrom("user").selectAll().execute());
console.log("\n[DELETE]"); const deleteResult = await client.deleteFrom("user").execute(); console.log(deleteResult); console.log("select:",await client.selectFrom("user").selectAll().execute());
|
実行結果は次の通り。
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
| $ deno run --allow-env --allow-read --allow-write --allow-net ./main.ts
[SELECT] query: select * from "user" parameters: [] select: []
[INSERT] query: insert into "user" ("name") values (?) returning "id", "name" parameters: [ "山田" ] { id: 1, name: "山田" } query: select * from "user" parameters: [] select: [ { id: 1, name: "山田", created_at: "2023-10-07 19:31:31" } ]
[UPDATE] query: update "user" set "name" = ? where "id" = ? returning "id", "name" parameters: [ "鈴木", 1 ] [ { id: 1, name: "鈴木" } ] query: select * from "user" parameters: [] select: [ { id: 1, name: "鈴木", created_at: "2023-10-07 19:31:31" } ]
[DELETE] query: delete from "user" parameters: [] [ DeleteResult {} ] query: select * from "user" parameters: [] select: []
|
CRUD すべてを確認できた。
リレーションも確認しておく
型だけ定義してある、food テーブルもマイグレーションを用意し、リレーションも確認する。
マイグレーションファイルは次の通り。
db/migrations/20231008043611create_food.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { Kysely, sql } from "npm:kysely@^0.25.0";
export async function up(db: Kysely<any>): Promise<void> { await db.schema .createTable("food") .addColumn("id", "integer", (col) => col.primaryKey()) .addColumn("user_id", "integer", (col) => col.notNull()) .addColumn("name", "text", (col) => col.notNull()) .addColumn( "created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull(), ) .execute(); }
export async function down(db: Kysely<any>): Promise<void> { await db.schema.dropTable("food").execute(); }
|
改めて main.ts を改修し、foodテーブルにもデータを登録する。
main.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
| import "https://deno.land/std@0.203.0/dotenv/load.ts"; import { client } from "./db/client.ts";
console.log("\n[SELECT]"); console.log("select:", await client.selectFrom("user").selectAll().execute()); console.log("select:", await client.selectFrom("food").selectAll().execute());
console.log("\n[INSERT]");
const transactionResult = await client.transaction().execute(async (trx) => { const insertResult = await trx .insertInto("user") .values({ name: "山田", }) .returning(["id"]) .executeTakeFirstOrThrow();
return await trx .insertInto("food") .values({ user_id: insertResult.id, name: "りんご", }) .returningAll() .executeTakeFirstOrThrow(); }); console.log(transactionResult);
console.log("\n[SELECT] 単独"); console.log("select:", await client.selectFrom("user").selectAll().execute()); console.log("select:", await client.selectFrom("food").selectAll().execute());
console.log("\n[INSERT] JOIN"); console.log("select:", await client.selectFrom("user").innerJoin('food', 'food.user_id', 'user.id').selectAll().execute()); console.log("select:", await client.selectFrom("user").innerJoin('food as f', 'f.user_id', 'user.id').select(["user.id", "user.name", "f.id as food_id", "f.name as food_name"]).execute());
|
実行結果は次の通り。
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
| $ deno run --allow-env --allow-read --allow-write --allow-net ./main.ts
[SELECT] query: select * from "user" parameters: [] select: [] query: select * from "food" parameters: [] select: []
[INSERT] query: insert into "user" ("name") values (?) returning "id" parameters: [ "山田" ] query: insert into "food" ("user_id", "name") values (?, ?) returning * parameters: [ 1, "りんご" ] { id: 1, user_id: 1, name: "りんご", created_at: "2023-10-07 20:02:48" }
[SELECT] 単独 query: select * from "user" parameters: [] select: [ { id: 1, name: "山田", created_at: "2023-10-07 20:02:47" } ] query: select * from "food" parameters: [] select: [ { id: 1, user_id: 1, name: "りんご", created_at: "2023-10-07 20:02:48" } ]
[INSERT] JOIN query: select * from "user" inner join "food" on "food"."user_id" = "user"."id" parameters: [] select: [ { id: 1, name: "山田", created_at: "2023-10-07 20:02:47", user_id: 1 } ] query: select "user"."id", "user"."name", "f"."id" as "food_id", "f"."name" as "food_name" from "user" inner join "food" as "f" on "f"."user_id" = "user"."id" parameters: [] select: [ { id: 1, name: "山田", food_id: 1, food_name: "りんご" } ]
|
トランザクション込みで、user から見て子になるfoodへのデータ登録と、select ができるのを確認できる。
Kysely を使って、Turso を Deno からざっくり使ってみた。
以前確認した Drizzle は、マイグレーションが上手く動かなかったりしたが、Kysely は少し自作すれば上手く動く。
今使うなら、Kysely が有力候補になってきそうである。
とても体験が良かった。
では。