typeorm の使い方

Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.

v0.3.283.8M/週MITORM / DB
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

TypeORM の使い方 — TypeScript対応のData-Mapper ORM徹底解説

一言でいうと

TypeORMは、TypeScriptとES2021+向けに設計されたData-Mapper/Active Record対応のORMです。デコレータベースでエンティティを定義し、MySQL、PostgreSQL、SQLite、MongoDB など主要なデータベースを統一的なAPIで操作できます。


どんな時に使う?

  1. TypeScriptプロジェクトで型安全にDB操作したい時 — エンティティ定義がそのまま型定義になるため、コンパイル時にクエリのミスを検知できます
  2. 複数のデータベースをサポートする必要がある時 — 接続設定を変えるだけでMySQL↔PostgreSQL↔SQLiteなどを切り替え可能です
  3. マイグレーション管理を含めた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();
}

類似パッケージとの比較

特徴TypeORMPrismaDrizzle ORMSequelize
言語サポートTypeScript / JavaScriptTypeScript / JavaScriptTypeScriptTypeScript / 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 オプションか QueryBuilderleftJoinAndSelect で明示的に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 },
});

⚠️ findwhereundefined を渡さない

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

比較記事