🎃

Drizzle ORM から TiDB Serverless に接続する

に公開

過去こちらの記事で Prisma ORM を用いてTiDB Serverlessへの接続を行いました。実行基盤としてはCloudflare Workersを用いています。
https://zenn.dev/kameoncloud/articles/5de3ad5f68a220

今日はもう一つのORMであるDrizzleを使ってみます。

Drizzle ORM とは?

https://orm.drizzle.team/
Prismaと同様に、TypeScriptの型や構造と、データベースのテーブル(行・列)を橋渡しする仕組みを提供します。TytpeScriptと連携させることで型安全なRDBMS呼び出しが可能となり、SQLに近い構文で、安全かつ型補完を得ながら記述できるため、ビジネスロジックと統一的な記述が可能となり、コード全体の可読性が高まります。また、SQLインジェクションなど不正なSQLを外部から実行させるという攻撃にも強くなることがメリットです。

Prismaは独自のPrisma Schema Languageによりデータ操作を行い、スキーマを .prisma ファイルで定義してからコード生成が必要ですが、DrizzleはTypeScriptファイル内に直接スキーマを記述でき、再コンパイル不要で反映されます。Prismaより軽量であることから、FaaSやエッジコンピューティング基盤の場合に、Prismaより優先的に採用されるケースがあります。

const result = await db.select().from(users).where(eq(users.id, 1));

今日はこちらを試していきます。

さっそくやってみる

0. TiDB Serverlessの起動

https://zenn.dev/kameoncloud/articles/e81735bb6bbdac
まずはこちらをもとにServerless Clusterを起動しておきます。接続に必要な情報を画面右上のConnectから取得しておきます。

はじめて起動した場合はパスワードが存在していませんので、Generate Passwordをクリックすると生成されます。

1.Project の初期化と Drizzle のインストール

mkdir drizzle-tidb-example
cd drizzle-tidb-example
npm init -y
npm install drizzle-orm mysql2 dotenv

2.ts ファイルの作成とテスト実行

db.ts
// db.ts
import { createPool } from 'mysql2/promise';
import { drizzle } from 'drizzle-orm/mysql2';
import { mysqlTable, int, varchar } from 'drizzle-orm/mysql-core';

// 接続設定(TiDB Serverless)
const pool = createPool({
  host: 'gateway01.us-west-2.prod.aws.tidbcloud.com',
  port: 4000,
  user: 'xxxxx.root',
  password: 'yyyyy',
  database: 'test',
  ssl: {
    rejectUnauthorized: true,
  },
});

// DrizzleのDBインスタンス生成
const db = drizzle(pool);

// スキーマ定義(usersテーブル)
const users = mysqlTable('users', {
  id: int('id').primaryKey().autoincrement(),
  name: varchar('name', { length: 255 }),
});

async function main() {
  // テーブル作成
  await db.execute(`
    CREATE TABLE IF NOT EXISTS users (
      id INT AUTO_INCREMENT PRIMARY KEY,
      name VARCHAR(255)
    );
  `);

  // データ挿入
  await db.insert(users).values({ name: 'Alice' });

  // データ取得
  const allUsers = await db.select().from(users);
  console.log(allUsers);
}

main().catch(console.error);

userpasswordは皆さんの環境用に書き換えておきます。

これで準備は完了です。

npx tsx db.ts

を実行すればテーブル作成,データ挿入,データ取得が行われ以下が表示されます。

[ { id: 1, name: 'Alice' } ]

TiDB Serverless 側でもデータが入っています。

eq , where の実行

次にwhere句を実行してみます。これには追加でeqが必要となります。
以下を追記します。

import { eq } from 'drizzle-orm';
const result = await db.select().from(users).where(eq(users.name, 'Alice'));

先ほどと同じ出力が出てくると思います。(実行のたびにInsertも走るのでAlice行がどんどん増えていきますが・・・)ちなみにDrizzleはCreate Table時は自動でCREATE TABLE IF NOT EXISTSを発行していますので、その部分はエラーとはならず処理がスキップされています。

order bylimit句もこのように記載して実行できます。

const result = await db.select().from(users).orderBy(users.id).limit(5);

Like や Count の実行

こちらも同様にまずはコンポーネントが必要となります。

import { like } from 'drizzle-orm';
import { count } from 'drizzle-orm';

const result = await db
  .select()
  .from(users)
  .where(like(users.name, '%Ali%'));

const result = await db
  .select({ count: count(users.id) })
  .from(users);

生SQLを実行

実はSQLをそのまま実行させることもできます。

const [rows] = await db.execute(`
  SELECT name, LENGTH(name) as length FROM users;
`);
console.log(rows);

ただこの方法はコード全体の可読性の観点からは、必要な場合を除いて定常的に用いるのはあまりお勧めできないです。特に複数人で作業を行う場合などは必ず統一された方法でデータ操作を行う方がいいかと思います。

例えば以下のようなケースは生SQLの発行が必要です。
1. TiDB Serverless 専用コマンドの実行
DrizzleはMySQLとしてTiDB Serverlessを呼び出しています。ここには方言差異が存在しており、TiDB独自コマンドを発行したい時

2. Drizzleが未サポートのSQL構文

WITH ranked_users AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id DESC) AS rank
  FROM users
)
SELECT * FROM ranked_users WHERE rank = 1;

Windows関数がまだ未サポートなので生SQLが必要になります。

3. 発行したSQLの最適化を行いたい場合
基本Drizzleは最適化されたSQLを発行してくれます。TiDB Serverless側でも以下の様に発行されたSQLを`確認できます。

Drizzle側でもtoSQL関数で確認できます。

import { sql } from 'drizzle-orm';
const stmt = db.select().from(users).where(eq(users.name, 'Alice'));
console.log('Generated SQL:', stmt.toSQL()); // ⬅ SQL構造出力
Generated SQL: {
  sql: 'select `id`, `name` from `users` where `users`.`name` = ?',
  params: [ 'Alice' ]
}

Discussion