TypeORM の使い方 — TypeScript対応のData-Mapper ORM徹底解説
一言でいうと
TypeORMは、TypeScriptとES2021+向けに設計されたData-Mapper/Active Record対応のORMです。デコレータベースでエンティティを定義し、MySQL、PostgreSQL、SQLite、MongoDB など主要なデータベースを統一的なAPIで操作できます。
どんな時に使う?
- TypeScriptプロジェクトで型安全にDB操作したい時 — エンティティ定義がそのまま型定義になるため、コンパイル時にクエリのミスを検知できます
- 複数のデータベースをサポートする必要がある時 — 接続設定を変えるだけでMySQL↔PostgreSQL↔SQLiteなどを切り替え可能です
- マイグレーション管理を含めたDB運用を行いたい時 — CLIによるマイグレーション生成・実行・リバートが組み込みで提供されています
インストール
# npm
npm install typeorm reflect-metadata
# yarn
yarn add typeorm reflect-metadata
# pnpm
pnpm add typeorm reflect-metadata
使用するデータベースに応じたドライバも必要です。
# PostgreSQL
npm install pg
# MySQL / MariaDB
npm install mysql2
# SQLite
npm install better-sqlite3
# MS SQL Server
npm install mssql
# MongoDB
npm install mongodb
tsconfig.json の設定
TypeORMはデコレータとreflect-metadataに依存するため、以下の設定が必須です。
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"lib": ["ES2021"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
}
}
アプリケーションのエントリポイント最上部で reflect-metadata をインポートしてください。
import "reflect-metadata";
基本的な使い方
最もよく使うパターンとして、エンティティ定義 → DataSource初期化 → CRUD操作の流れを示します。
エンティティ定義
// entity/User.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: "varchar", length: 100 })
name: string;
@Column({ type: "varchar", length: 255, unique: true })
email: string;
@Column({ type: "boolean", default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
DataSource の初期化
// data-source.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "password",
database: "myapp",
synchronize: true, // 開発時のみtrue。本番ではマイグレーションを使う
logging: true,
entities: [User],
migrations: ["src/migration/*.ts"],
});
CRUD 操作
// index.ts
import "reflect-metadata";
import { AppDataSource } from "./data-source";
import { User } from "./entity/User";
async function main() {
// 接続初期化
await AppDataSource.initialize();
console.log("DataSource initialized");
const userRepository = AppDataSource.getRepository(User);
// Create
const user = userRepository.create({
name: "田中太郎",
email: "tanaka@example.com",
});
await userRepository.save(user);
// Read
const found = await userRepository.findOneBy({ email: "tanaka@example.com" });
console.log("Found user:", found);
// Update
if (found) {
found.name = "田中次郎";
await userRepository.save(found);
}
// Delete
if (found) {
await userRepository.remove(found);
}
await AppDataSource.destroy();
}
main().catch(console.error);
よく使うAPI
1. Repository — 基本的なCRUD
const repo = AppDataSource.getRepository(User);
// 全件取得
const allUsers = await repo.find();
// 条件付き取得(複数件)
const activeUsers = await repo.find({
where: { isActive: true },
order: { createdAt: "DESC" },
take: 10,
skip: 0,
});
// 1件取得(見つからなければnull)
const user = await repo.findOneBy({ id: 1 });
// 件数取得
const count = await repo.countBy({ isActive: true });
// 一括挿入
await repo.save([
{ name: "User A", email: "a@example.com" },
{ name: "User B", email: "b@example.com" },
]);
// 条件指定で削除
await repo.delete({ isActive: false });
2. QueryBuilder — 複雑なクエリの構築
const users = await AppDataSource.getRepository(User)
.createQueryBuilder("user")
.where("user.isActive = :isActive", { isActive: true })
.andWhere("user.name LIKE :name", { name: "%田中%" })
.orderBy("user.createdAt", "DESC")
.skip(0)
.take(20)
.getMany();
// サブクエリ・JOIN も可能
const usersWithPosts = await AppDataSource.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.posts", "post")
.where("post.isPublished = :published", { published: true })
.getMany();
3. Relations — リレーション定義
// entity/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { User } from "./User";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: "varchar", length: 200 })
title: string;
@Column({ type: "text" })
content: string;
@Column({ type: "boolean", default: false })
isPublished: boolean;
@ManyToOne(() => User, (user) => user.posts, { onDelete: "CASCADE" })
author: User;
}
// entity/User.ts にリレーションを追加
import { OneToMany } from "typeorm";
import { Post } from "./Post";
@Entity()
export class User {
// ...既存のカラム
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}
// リレーション付きで取得
const userWithPosts = await userRepository.findOne({
where: { id: 1 },
relations: { posts: true },
});
4. Migrations — マイグレーション管理
# マイグレーション生成(エンティティの差分から自動生成)
npx typeorm-ts-node-commonjs migration:generate src/migration/AddUserTable -d src/data-source.ts
# マイグレーション実行
npx typeorm-ts-node-commonjs migration:run -d src/data-source.ts
# マイグレーションのリバート
npx typeorm-ts-node-commonjs migration:revert -d src/data-source.ts
生成されるマイグレーションファイルの例:
// src/migration/1700000000000-AddUserTable.ts
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserTable1700000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "user" (
"id" SERIAL PRIMARY KEY,
"name" VARCHAR(100) NOT NULL,
"email" VARCHAR(255) NOT NULL UNIQUE,
"isActive" BOOLEAN DEFAULT true,
"createdAt" TIMESTAMP DEFAULT now(),
"updatedAt" TIMESTAMP DEFAULT now()
)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "user"`);
}
}
5. Transaction — トランザクション制御
// 方法1: DataSource.transaction(推奨)
await AppDataSource.transaction(async (manager) => {
const user = manager.create(User, {
name: "佐藤花子",
email: "sato@example.com",
});
await manager.save(user);
const post = manager.create(Post, {
title: "初投稿",
content: "こんにちは",
author: user,
});
await manager.save(post);
// 例外が発生すれば自動ロールバック
});
// 方法2: QueryRunner で手動制御
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(User, { name: "手動", email: "manual@example.com" });
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
類似パッケージとの比較
| 特徴 | TypeORM | Prisma | Drizzle ORM | Sequelize |
|---|---|---|---|---|
| 言語サポート | TypeScript / JavaScript | TypeScript / JavaScript | TypeScript | TypeScript / JavaScript |
| パターン | Data Mapper + Active Record | 独自(Prisma Client) | Data Mapper風 | Active Record |
| スキーマ定義 | デコレータ(コード内) | .prisma ファイル | TypeScriptコード | クラス / オブジェクト |
| 型安全性 | ○(デコレータ依存) | ◎(スキーマから自動生成) | ◎(TypeScript推論) | △ |
| マイグレーション | CLI組み込み | CLI組み込み | CLI組み込み | CLI組み込み |
| MongoDB対応 | ○ | ○ | × | × |
| 学習コスト | 中 | 低〜中 | 低 | 中 |
| エコシステム成熟度 | ◎(2016年〜) | ◎ | ○(比較的新しい) | ◎(最も歴史が長い) |
選定の目安:
- 型安全性を最重視 → Prisma または Drizzle
- デコレータベースが好み / NestJSと組み合わせる → TypeORM
- SQLに近い書き味が好み → Drizzle
- 既存のJSプロジェクトの移行 → Sequelize
注意点・Tips
⚠️ synchronize: true は本番で絶対に使わない
// data-source.ts
export const AppDataSource = new DataSource({
// ...
synchronize: process.env.NODE_ENV !== "production", // 開発時のみ
});
synchronize: true はエンティティの変更をDBスキーマに自動反映しますが、データの消失やカラム削除が予告なく実行される可能性があります。本番環境では必ずマイグレーションを使ってください。
⚠️ N+1問題に注意
リレーションの遅延読み込みはN+1問題を引き起こします。relations オプションか QueryBuilder の leftJoinAndSelect で明示的にJOINしてください。
// ❌ N+1が発生しやすいパターン
const users = await userRepository.find();
for (const user of users) {
console.log(user.posts); // undefinedか、lazy loadingで毎回クエリ発行
}
// ✅ 明示的にリレーションを読み込む
const users = await userRepository.find({
relations: { posts: true },
});
⚠️ find の where に undefined を渡さない
where オブジェクトのプロパティに undefined を渡すと、その条件が無視されて意図しない全件取得になる場合があります。
// ❌ 危険:nameがundefinedだと条件なしになる
const users = await repo.find({
where: { name: someVariable }, // someVariableがundefinedの場合…
});
// ✅ 条件を動的に組み立てる
const where: FindOptionsWhere<User> = {};
if (someVariable !== undefined) {
where.name = someVariable;
}
const users = await repo.find({ where });
💡 ログ出力でデバッグを効率化
export const AppDataSource = new DataSource({
// ...
logging: ["query", "error"], // "query" | "error" | "schema" | "warn" | "info" | "log" | "migration"
logger: "advanced-console", // 色付きログ
});
💡 カスタムリポジトリパターン(v0.3.x)
v0.3.x では extends Repository ではなく、DataSource.getRepository().extend() を使います。
// repository/UserRepository.ts
import { AppDataSource } from "../data-source";
import { User } from "../entity/User";
export const UserRepository = AppDataSource.getRepository(User).extend({
async findByEmail(email: string): Promise<User | null> {
return this.findOneBy({ email });
},
async findActiveUsers(): Promise<User[]> {
return this.find({
where: { isActive: true },
order: { createdAt: "DESC" },
});
},
});
// 使用側
const user = await UserRepository.findByEmail("tanaka@example.com");
💡 NestJS との統合
TypeORMはN