🐕

TiDB Serverless への接続:TypeORM デコレーターで直感的なモデル定義

に公開

過去の記事で3種類のORMを使ってTiDB Serverlessへの接続を試してきました。

Drizzle
https://zenn.dev/kameoncloud/articles/c15ffb44d177b9

Prisma
https://zenn.dev/kameoncloud/articles/5de3ad5f68a220

Sequelize
https://zenn.dev/kameoncloud/articles/61023ed061ce55

今日は4つめのTypeORMを使ってみたいと思います。

TypeORM とデコレーター

TypeScript および JavaScript 向けに開発されたORM(Object-Relational Mapping)ライブラリで、過去に紹介した 3つのORMとは異なり、クラスベースの構文とデコレーターを用いてデータベースのテーブルやカラムを直感的に定義できる点が特徴のようです。

Active Record と Data Mapper の両スタイルをサポートしており、柔軟な開発が可能です。たとえば Prisma は、静的なスキーマ定義ファイル(schema.prisma)に基づいて型情報やクエリクライアントをモデルとして自動生成することで、型安全性の高い開発が可能になります。
このように、モデルとクエリ処理を分離する構成は、Data Mapper パターンの一形態といえます。

一方、TypeORM の Active Record スタイルでは、事前のコード生成を行わずにエンティティクラスを定義し、そのままデータベース操作が可能です。

ChatGPT先生に違いを制してもらうと以下の表となりました。わかりやすい!ありがとうございます。

このとき、各プロパティやクラスに意味づけを行うための仕組みとして使われるのがデコレーターです。
デコレーターは、クラスやプロパティに「これはテーブル」「これはカラム」という“タグ”のような役割をメタ情報として与えてくれます。デコレーターによって、実行時に構造が明確になるため、ミスの混入も抑えやすくなっています。

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;
}

このコードは、User というクラスをデータベースのテーブルとして定義しています。

@Entity():このクラスがテーブルになることを示します。
@PrimaryGeneratedColumn():主キーとして使われ、自動で番号が付きます。
@Column():このプロパティがテーブルのカラムになります。

このように、デコレーターを使ってクラスにテーブルの情報を埋め込むことで、ミスの混入を軽減することができるようです。

さっそくやってみる

まずは必要ライブラリのインストールです。

npm install typeorm mysql2

前回のsequalize同様MySQLへの接続にはmysql2を使っています。
次にreflect-metadetaをインストールします。

npm install reflect-metadata

これは先ほどご紹介したデコレータが定義したメタ情報の動作に必要なライブラリです。TypeORMはデコレータをサポートしていることが特徴ですが、デコレータそのものはTypeScript本体の機能であり、TypeORM固有のものではありません。
デコレータのサポートはTypeScriptがJavaScriptに先行している状況で、このためJavaScriptでTypeORMのスクリプトを開発した場合、デコレータは使えなくなってしまいます。
TypeScriptで開発されたスクリプトをJavaScriptにトランスパイルする際にデコレータのメタ情報を保存しておいてくれるのが、reflect-metadataになります。

次に以下のソースをコピペして保存します。

db.ts
import 'reflect-metadata';
import { DataSource, Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
class User {
  @PrimaryGeneratedColumn()
  id!: number; // ! を追加

  @Column()
  name!: string; // ! を追加
}

const AppDataSource = new DataSource({
  type: 'mysql',
  host: 'gateway01.us-west-2.prod.aws.tidbcloud.com',
  port: 4000,
  username: '<userid>',
  password: '<password>',
  database: 'test',
  ssl: { rejectUnauthorized: true },
  entities: [User],
  synchronize: true,
  logging: true,
});

AppDataSource.initialize()
  .then(async () => {
    console.log('✅ Connected to TiDB Serverless');
    const user = new User();
    user.name = 'Alice';
    await AppDataSource.manager.save(user);
    console.log('👤 User saved:', user);
  })
  .catch((error) => {
    console.error('❌ Connection error:', error);
  });

<userid><password>は皆さんの環境ごとに置き換えてください。
次にtsconfig.jsonを同じ場所に作成します。

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "strictPropertyInitialization": false,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["./**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

ではいよいよテストです。
npx ts-node db.tsで実行を行うと以下の出力が出ます。

query: SELECT VERSION() AS `version`
query: START TRANSACTION
query: SELECT DATABASE() AS `db_name`
query: SELECT `TABLE_SCHEMA`, `TABLE_NAME`, `TABLE_COMMENT` FROM `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = 'test' AND
 `TABLE_NAME` = 'user'
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'test' AND `TABLE_NAME` = 'typeorm_metadata'
query: CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB
query: COMMIT
✅ Connected to TiDB Serverless
query: START TRANSACTION
query: INSERT INTO `user`(`id`, `name`) VALUES (DEFAULT, ?) -- PARAMETERS: ["Alice"]
query: COMMIT
👤 User saved: User { id: 1, name: 'Alice' }

Discussion