2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScript用ORM「Drizzle ORM」をDBファーストで使う試み(SQLite編)

Last updated at Posted at 2024-01-21

本記事で行うこと

以前、Go言語用のORM「Bun」をデータベースファーストで使えるか試しました。
それと同様に、TypeScript用のORM「Drizzle ORM」をデータベースファーストで使えるか試します。
WebフレームワークはHonoを使用します。

DBファーストで開発したい理由

理由は以前書いた記事と全く同じです。

テーブルを作る

では作業に移ります。
DBファーストですので、まずDDLを書きます。

CREATE TABLE foo (
    col_text           TEXT    NOT NULL,
    col_text_null      TEXT,
    col_int            INTEGER NOT NULL,
    col_int_null       INTEGER,
    col_real           REAL    NOT NULL,
    col_real_null      REAL,
    col_timestamp      REAL    NOT NULL,
    col_timestamp_null REAL
) STRICT;

上記DDLの意図

SQLite3のSTRICTテーブルで使える型は以下に限られています。

  • INT
  • INTEGER
  • REAL
  • TEXT
  • BLOB
  • ANY

このうち、私のプロジェクトで使う可能性が高い以下の型を試します。

  • TEXT
  • INTEGER
  • REAL

加えて、日付・時刻も試します。
日付・時刻は、以下の型で保存できます。

  • TEXT:ISO-8601形式
  • REAL:ユリウス通日
  • INTEGER:1970年からの秒数

本記事では日付・時刻をREAL型で保存することにしました。理由は、

  • INTEGER型で持つと精度が秒になるが、REAL型やTEXT型は秒の小数の精度まで持てる
  • TEXT型は保存フォーマットに気を遣う必要があって面倒だし、バイト数もREAL型より大きい

以上が上記DDLの意図です。

テスト用データをINSERTする

作成したfooテーブルに、以下のSQLでテスト用データを2件INSERTしました。

INSERT INTO foo
(
    col_text,
    col_text_null,
    col_int,
    col_int_null,
    col_real,
    col_real_null,
    col_timestamp,
    col_timestamp_null
)
VALUES
(
    'text_data',
    NULL,
    1234567890123456789,
    NULL,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    NULL,
    JULIANDAY('now'),
    NULL
),
(
    'text_data',
    'text_data',
    1234567890123456789,
    1234567890123456789,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    JULIANDAY('now'),
    JULIANDAY('now')
);

INSERTしたら、fooテーブルにSELECT文を発行してみます。

SELECT
    col_text,
    col_text_null,
    col_int,
    col_int_null,
    col_real,
    col_real_null,
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp,
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null
FROM
    foo;

SELECT文の結果です。

image.png

HonoとDrizzle ORMを使って、fooテーブルにCRUD操作するWeb APIサーバーを作ってみる

それでは、TypeScriptからDrizzle ORMを使っていきます。

プロジェクト作成

プロジェクト名は「exercise_drizzle_sqlite」にしました。
どのテンプレートを使うか聞かれたので、「bun」を選択しました。
参考ページ: https://hono.dev/getting-started/basic

bunx create-hono exercise_drizzle_sqlite

パッケージインストール

プロジェクトディレクトリ直下で、以下を実行しました。
参考ページ: https://orm.drizzle.team/docs/get-started-sqlite#better-sqlite3

bun add drizzle-orm better-sqlite3
bun add -D drizzle-kit

DBからTypeScriptのスキーマを自動生成

DBファーストなので、コードからDBスキーマを生成するのではなく、DBスキーマからコードを生成する順になりました。
プロジェクトディレクトリ直下で、以下のdrizzle-kitコマンドを実行しました。
urlオプションには、SQLite3のデータファイルを指定します。

drizzle-kit introspect:sqlite --driver=better-sqlite --url=/home/user/DataGripProjects/sqlite/exercise.sqlite3

ディレクトリ構造とファイル

drizzle/ディレクトリ配下のファイルは、drizzle-kitコマンドを実行したときに自動生成されたものです。

Project Root
  ├── drizzle/
  │     ├── meta/
  │     │     ├── 0000_snapshot.json
  │     │     └── journal.json
  │     ├── 0000_violet_mentallo.sql
  │     └── schema.ts
  ├── src/
  │     └── index.ts
  ├── package.json
  ├── README.md
  └── tsconfig.json

drizzle/schema.ts

自動生成されたコードです。
src/index.tsからimportして使用します。

drizzle/schema.ts
import { sqliteTable, AnySQLiteColumn, text, integer, real } from "drizzle-orm/sqlite-core"
  import { sql } from "drizzle-orm"

export const foo = sqliteTable("foo", {
	colText: text("col_text").notNull(),
	colTextNull: text("col_text_null"),
	colInt: integer("col_int").notNull(),
	colIntNull: integer("col_int_null"),
	colReal: real("col_real").notNull(),
	colRealNull: real("col_real_null"),
	colTimestamp: real("col_timestamp").notNull(),
	colTimestampNull: real("col_timestamp_null"),
});

src/index.ts

メインのコードです。

src/index.ts
import { Hono } from "hono";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { sql, eq } from "drizzle-orm";
import { foo } from "../drizzle/schema"

const sqlite = new Database("/home/user/DataGripProjects/sqlite/exercise.sqlite3");
const db = drizzle(sqlite, { logger: true });

const app = new Hono()
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))

app.get("/api/selectFoo", (c) => {
    const resultSet = db.select(
        {
            colText: foo.colText,
            colTextNull: foo.colTextNull,
            colInt: sql`CAST(col_int AS TEXT)`,
            colIntNull: sql`CAST(col_int_null AS TEXT)`,
            colReal: foo.colReal,
            colRealNull: foo.colRealNull,
            colTimestamp: sql`STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp)`,
            colTimestampNull: sql`STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null)`,
        }
        ).from(foo)
        .all()

    return c.json(resultSet)
})

app.put("/api/updateFoo", async (c) => {
    await db.update(foo)
        .set({
            colTextNull: "ABC",
        })
        .where(sql`${foo.colTextNull} IS NULL`)

    return c.json({ message: "ok", ok: true })
})

app.post("/api/insertFoo", async (c) => {
    await db.insert(foo)
        .values({
            colText: "XYZ",
            colTextNull: null,
            colInt: 123,
            colIntNull: null,
            colReal: 3.14,
            colRealNull: null,
            colTimestamp: sql`JULIANDAY('now')`,
            colTimestampNull: null
        })

    return c.json({ message: "ok", ok: true })
})

app.delete("/api/deleteFoo", async (c) => {
    await db.delete(foo)
        .where(eq(foo.colText, "XYZ"))

    return c.json({ message: "ok", ok: true })
})

export default app

package.json

package.json
{
  "scripts": {
    "dev": "bun run --hot src/index.ts"
  },
  "dependencies": {
    "better-sqlite3": "^9.3.0",
    "drizzle-orm": "^0.29.3",
    "hono": "^3.12.6"
  },
  "devDependencies": {
    "@types/bun": "^1.0.0",
    "drizzle-kit": "^0.20.13"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

CRUD操作

それでは、CRUD操作について一つ一つ見ていきます。

SELECT文

まずはSELECT文です。

SELECT用ハンドラの中身

ハンドラの中身を見ていきます。
SQLが透けて見えますね。
私みたいにSQLを書きたい人には、嬉しいORMだと思いました。

「sql``」で囲った部分は、Drizzle ORMがSQLを生成するときにそのまま使われるので、DB側の構文やファンクションを使用できます。
DBでINT型で定義された列は、TypeScriptのnumber型で受け取ると、桁数が多い場合に丸め誤差が生じるので、TEXT型にキャストしました。

    const resultSet = db.select(
        {
            colText: foo.colText,
            colTextNull: foo.colTextNull,
            colInt: sql`CAST(col_int AS TEXT)`,
            colIntNull: sql`CAST(col_int_null AS TEXT)`,
            colReal: foo.colReal,
            colRealNull: foo.colRealNull,
            colTimestamp: sql`STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp)`,
            colTimestampNull: sql`STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null)`,
        }
        ).from(foo)
        .all()

    return c.json(resultSet)

Drizzle ORMが生成したSQL

上記コードからDrizzle ORMが生成したSQLを、整形して見やすくしました。

select
    "col_text",
    "col_text_null",
    CAST(col_int AS TEXT),
    CAST(col_int_null AS TEXT),
    "col_real",
    "col_real_null",
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp),
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null)
from
    "foo"

selectFoo APIの実行結果

selectFoo APIを呼び出した結果です。
レスポンスは、ハンドラの最後の一行で、結果表をそのままjsonにして返しているだけですが、きれいなレスポンスが生成できていますね。
シンプルなコードできれいなレスポンスを返せているのはポイントが高いです。

[
    {
        "colText": "text_data",
        "colTextNull": null,
        "colInt": "1234567890123456789",
        "colIntNull": null,
        "colReal": 3.141592653589793,
        "colRealNull": null,
        "colTimestamp": "2024-01-21T01:47:36.868Z",
        "colTimestampNull": null
    },
    {
        "colText": "text_data",
        "colTextNull": "text_data",
        "colInt": "1234567890123456789",
        "colIntNull": "1234567890123456789",
        "colReal": 3.141592653589793,
        "colRealNull": 3.141592653589793,
        "colTimestamp": "2024-01-21T01:47:36.868Z",
        "colTimestampNull": "2024-01-21T01:47:36.868Z"
    }
]

image.png

UPDATE文

次はUPDATE文を見ていきます。

UPDATE用ハンドラの中身

ハンドラのクエリー部分を見ます。
col_text_null列がNULLの行について、同じ列を"ABC"に更新する内容です。

    await db.update(foo)
        .set({
            colTextNull: "ABC",
        })
        .where(sql`${foo.colTextNull} IS NULL`)

Drizzle ORMが生成したSQL

上記コードからDrizzle ORMが生成したSQLを、整形して見やすくしました。

update
    "foo"
set
    "col_text_null" = ?
where
    "foo"."col_text_null" IS NULL

-- params: ["ABC"]

updateFoo APIの実行結果

updateFoo APIを呼び出した結果です。
col_text_nullがNULLだった行の意図した列が、意図した値に更新されました。

image.png

image.png

INSERT文

次はINSERT文を見ていきます。
col_text列が"XYZ"の行を挿入する内容です。

INSERT用ハンドラの中身

ハンドラのクエリー部分を見ます。

    await db.insert(foo)
        .values({
            colText: "XYZ",
            colTextNull: null,
            colInt: 123,
            colIntNull: null,
            colReal: 3.14,
            colRealNull: null,
            colTimestamp: sql`JULIANDAY('now')`,
            colTimestampNull: null
        })

Drizzle ORMが生成したSQL

上記コードからDrizzle ORMが生成したSQLを、整形して見やすくしました。

insert into "foo" (
    "col_text",
    "col_text_null",
    "col_int",
    "col_int_null",
    "col_real",
    "col_real_null",
    "col_timestamp",
    "col_timestamp_null"
)
values (
    ?,
    ?,
    ?,
    ?,
    ?,
    ?,
    JULIANDAY('now'),
    ?
)


-- params: [
    "XYZ",
    null,
    123,
    null,
    3.14,
    null,
    null
]

insertFoo APIの実行結果

insertFoo APIを呼び出した結果です。
3行目に、意図した行がINSERTされたことを確認できました。

image.png

image.png

DELETE文

次はDELETE文を見ていきます。

DELETE用ハンドラの中身

ハンドラのクエリー部分を見ます。
col_text列が"XYZ"の行を削除する内容です。

    await db.delete(foo)
        .where(eq(foo.colText, "XYZ"))

Drizzle ORMが生成したSQL

上記コードからDrizzle ORMが生成したSQLを、整形して見やすくしました。

delete from
    "foo"
where
    "foo"."col_text" = ?

-- params: ["XYZ"]

deleteFoo APIの実行結果

deleteFoo APIを呼び出した結果です。
意図した行(3行目)が正常に削除されました。

image.png

image.png

まとめ

Drizzle ORMをDBファーストで使ってみたところ、問題なく動いてくれました。
また、SQLが透けて見える書き方ができて、SQLを書きたい私でも使いたいと思わせてくれるORMでした。
以前の記事で試したGo言語用ORMのBunと、割と似た感覚で使えそうです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?