🐙

Drizzle ORM と TiDB Serverless の連携 (Node 20.x)

2024/07/09に公開

TiDB Serverless には Serverless Driver というTiDB Serverless クラウドデータベースサービスに接続し、操作するためのライブラリが用意されています。これにより、開発者はTiDBクラウドデータベースに対してアプリケーションコードから直接クエリを実行したり、データの読み書きが実行可能です。
Serverless Driver は Node.jsアプリケーションからこのサービスに接続するためのJavaScriptライブラリで構成されています。
https://docs.pingcap.com/tidbcloud/serverless-driver

これによりステートレスなサーバレス基盤からTiDB Serverlessが利用可能になります。例えばCloudflare Workers であれば以下にその手順がまとまっています。
https://zenn.dev/kameoncloud/articles/99d3ed9d5ce4fd
https://zenn.dev/kameoncloud/articles/5de3ad5f68a220

Drizzle ORM が新しく TiDB Serverless Driver に対応していました。

Drizzle ORM

TypeScriptおよびJavaScript向けのオブジェクト関係マッピング(ORM)ライブラリ群です。ORMを使うことで、データベースとプログラミング言語のオブジェクトとの間のマッピングを簡素化し、開発者がデータベース操作をオブジェクト指向の方法で実行できるようにするツールです。
通常のSQLを書き込むことなくデータベースの操作が可能となるため、開発効率の向上だけではなく、SQL Injectionへの対応も可能となるためセキュリティの向上が期待できます。
その中でも Drizzle はTypeScriptとネイティブに統合されており型宣言をベースとしたクエリの構築が可能であることが特徴です。

ORMとしてよく聞くPrismaと比べると一般的には基本的な機能にフォーカスしている分、軽量でありTypeScriptサポートがより強力であることが特徴です。

やってみる

https://docs.pingcap.com/tidbcloud/serverless-driver-drizzle-example
こちらの手順をもとに作業を進めていきます。試したところこちらの手順はNode 18.x 環境用でありNode 20.xではどうさしませんでしたので、以下の手順はNode 20.xで進めていきます。

必要なもの

Node.js >= 20.0.0.
npm
TiDB Serverless クラスター

TiDB Serverless クラスターがまだない場合は、以下の記事を参考に起動してみて下さい。
https://zenn.dev/kameping/articles/2248cb2833785e

まずは作業用ディレクトリを作ります。

mkdir drizzle-node-example
cd drizzle-node-example

Node.js プロジェクトとしてpackage.jsonを初期化します。

npm init -y

次にTiDB Serverless Driverをインストールします。

npm install @tidbcloud/serverless

インストールができたらDrizzle ORM の TiDB Serverless 用モジュールをインストールします。

npm install drizzle-orm @tidbcloud/serverless

package.jsonの一番最後の}の前に以下の行を追加します。

  "type": "module"

以下のような状態になります。

package.json
{
  "name": "drizzle-node-example",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@tidbcloud/serverless": "^0.1.1",
    "drizzle-orm": "^0.31.4"
  },
  "type": "module"
}

次に同じディレクトリにtsconfig.jsonを以下の内容で作成します。

tsconfig.json
{
  "compilerOptions": {
    "module": "ES2022",
    "target": "ES2022",
    "moduleResolution": "node",
    "strict": false,
    "declaration": true,
    "outDir": "dist",
    "removeComments": true,
    "allowJs": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}

TiDB Cloud のマネージメントコンソールから、右上のConnectボタンを押して、TiDB Serverless クラスターへの接続文字列を入手します。

SQL Editorで以下のクエリを実行してテスト用usersテーブルを作成しておきます。

CREATE TABLE `test`.`users` (
 `id` BIGINT PRIMARY KEY auto_increment,
 `full_name` TEXT,
 `phone` VARCHAR(256)
);

以下のようにテーブルが作成されていれば完了です。

次に作業ディレクトリに戻り以下の内容でhello-world.tsを作成します。

hello-world.ts
import { connect } from '@tidbcloud/serverless';
import { drizzle } from 'drizzle-orm/tidb-serverless';
import { mysqlTable, serial, text, varchar } from 'drizzle-orm/mysql-core';

// Initialize
const client = connect({url:"mysql://4JYpfj5Y7etkbg1.root:<password>@gateway01.eu-central-1.prod.aws.tidbcloud.com:4000/test" });
const db = drizzle(client);

// Define schema
export const users = mysqlTable('users', {
  id: serial("id").primaryKey(),
  fullName: text('full_name'),
  phone: varchar('phone', { length: 256 }),
});
export type User = typeof users.$inferSelect; // return type when queried
export type NewUser = typeof users.$inferInsert; // insert type

// Insert and select data
const user: NewUser = { fullName: 'John Doe', phone: '123-456-7890' };
await db.insert(users).values(user)
const result: User[] = await db.select().from(users);
console.log(result);

<password>は皆さん固有の文字列に置き換えてください。
次にTypeScriptの実行環境に必要なモジュールをインストールします。

npm install -g ts-node
npm i --save-dev @types/node

この後公式手順ではts-node --esm hello-world.tsで実行となっていますが、Node 20.x環境だとERR_UNKNOWN_FILE_EXTENSIONエラーになります。tsxというモジュールをインストールします。

npm i -D tsx

インストールが完了したら以下のコマンドで実行してみます。

npx tsx hello-world.ts
[ { id: 1, fullName: 'John Doe', phone: '123-456-7890' } ]

中身を見てみる

hello-world.tsの中身を見ていきます。

  1. 必要なライブラリのインポート
import { connect } from '@tidbcloud/serverless';
import { drizzle } from 'drizzle-orm/tidb-serverless';
import { mysqlTable, serial, text, varchar } from 'drizzle-orm/mysql-core';

1行目がTiDB Serverless Driver、2行目がTiDB Serverless Driver用Drizzle ORMモジュール、3行目がTiDB Serverless Driver用Drizzle ORMモジュールインスタンスが利用可能な機能群です。

  1. 初期化
// Initialize
const client = connect({ url: "mysql://4JYpfj5Y7etkbg1.root:<password>@gateway01.eu-central-1.prod.aws.tidbcloud.com:4000/test" });
const db = drizzle(client);

dbがDrizzle ORM 用インスタンスです。

  1. スキーマの定義
    PrismaなどのORM異なりDrizzleは軽量であることを前段で説明しました。その代わりスキーマの定義はシンプルに直接ここで行います。
// Define schema
export const users = mysqlTable('users', {
  id: serial("id").primaryKey(),
  fullName: text('full_name'),
  phone: varchar('phone', { length: 256 }),
});
export type User = typeof users.$inferSelect; // return type when queried
export type NewUser = typeof users.$inferInsert; // insert type

userstest.usersテーブルのインスタンスです。
NewUserはDrizzle経由でテーブルにデータをInsertするための型です。DrizzleはTypeScriptとネイティブに統合されている、という部分です。なおUserは同様にSelect用の型です。

  1. SQLの実行
const user: NewUser = { fullName: 'John Doe', phone: '123-456-7890' };
await db.insert(users).values(user)
const result: User[] = await db.select().from(users);
console.log(result);
const user: NewUser = { fullName: 'John Doe', phone: '123-456-7890' };

は前述の通りInsertです。SQLにすると以下を意味します。

INSERT INTO test.users (full_name,phone) VALUES ('John Doe', '123-456-7890');

以下はSelectです。

const result: User[] = await db.select().from(users);

SQLですと以下になります。

select * from test.users;

次に少し書き換えてDeleteを試してみます。

// Insert and select data
//const user: NewUser = { fullName: 'John Doe', phone: '123-456-7890' };
//await db.insert(users).values(user)
await db.delete(users);
const result: User[] = await db.select().from(users);
console.log(result);

これによりテーブルのデータは全て削除されます。一つ注意点です。Drizzleでは現在DeleteにおけるWhere句はPostgreSQLかSQL Liteのみが対応しておりMySQLでは対応していないようです。これだと使いづらいですね。Updateも同様でした。ただ、ドキュメントが古いだけの可能性があります。
https://orm.drizzle.team/docs/operators
の通りにeqをimportすると動作しました。

import { eq } from "drizzle-orm";

をヘッダーパートに付与して以下を実行すると動作しました。

// Insert and select data
const user: NewUser = { fullName: 'John Doe', phone: '123-456-7890' };
await db.insert(users).values(user)
await db.delete(users).where(eq(users.fullName, 'John Doe'));
const result: User[] = await db.select().from(users);
console.log(result);

Updateも同様に行けました。

// Insert and select data
const user: NewUser = { fullName: 'John Doe', phone: '123-456-7890' };
await db.insert(users).values(user)
await db.update(users)
  .set({ fullName: 'Mr. Dan' })
  .where(eq(users.fullName, 'John Doe'));
const result: User[] = await db.select().from(users);
console.log(result);

Discussion