はじめに
TypeORMは、TypeScript/JavaScript向けの老舗ORMですね。
GitHubで36,200スター以上、5,700以上のコミット、1,100人以上のコントリビューターという数字を見ると、長年にわたって多くの開発者に支持されているのがわかります。個人的には、Node.jsでエンタープライズ向けのバックエンドを書くなら、TypeORMは安定した選択肢だと思っています。
正直なところ、最初は「ORMって学習コストが高そう」と思っていたんですよ。でも実際にTypeORMを使ってみたら、デコレータベースでエンティティを定義できて、直感的に書ける。SQLを知っている人なら、すぐに馴染める感覚でした。
ライセンスはMITで、安心して使えます。
TypeORMとは
TypeORMは「Code with Confidence. Query with Power.」を掲げるORMです。2016年から開発が続いており、Node.jsのORMとしては老舗の部類に入る。
特筆すべきは、DataMapperパターンとActiveRecordパターンの両方をサポートしている点。プロジェクトの規模や設計方針に合わせて、最適なアプローチを選択できます。
特徴・メリット
1. TypeScriptファーストの設計
TypeORMの最大の魅力は、TypeScriptとの相性の良さですね。
最初からTypeScriptで書かれており、デコレータを使ったエンティティ定義は型安全。エディタの補完も効くし、リファクタリングも安心してできる。
30代になって思うのは、型があるとバグが減るということ。
2. 豊富なデータベースサポート
TypeORMは10種類以上のデータベースに対応しています。
リレーショナルDB:
- MySQL / MariaDB
- PostgreSQL
- SQLite
- MS SQL Server
- Oracle
- CockroachDB
- SAP HANA
- Google Spanner
NoSQL:
- MongoDB
これだけ対応していれば、プロジェクトのデータベース選定で困ることはまずない。
3. 2つの設計パターン対応
TypeORMの特徴的な点は、DataMapperとActiveRecordの両パターンに対応していること。
ActiveRecordパターン: エンティティ自体にメソッドを持たせる。シンプルなアプリに向いている。
DataMapperパターン: エンティティとリポジトリを分離。テストしやすく、大規模アプリに向いている。
個人的には、小規模ならActiveRecord、中〜大規模ならDataMapperという使い分けをしています。
4. 強力なマイグレーション機能
スキーマの変更を自動検出して、マイグレーションファイルを生成してくれます。
手動でSQLを書く必要がないので、時短になる。チーム開発でも、スキーマの変更履歴が追跡できて安心。
5. QueryBuilderの柔軟性
複雑なクエリも、QueryBuilderでチェーンして書けます。
const users = await userRepository
.createQueryBuilder("user")
.leftJoinAndSelect("user.posts", "post")
.where("user.age > :age", { age: 18 })
.orderBy("user.createdAt", "DESC")
.getMany();
SQLライクな記法で、直感的に書ける。
6. マルチプラットフォーム対応
Node.js以外でも動きます。
- Node.js
- ブラウザ
- React Native
- Electron
- Cordova
モバイルアプリでローカルDBを使いたいときにも対応できる。
インストール方法
前提条件
Node.js 16以上を推奨。
基本インストール
# npm
npm install typeorm reflect-metadata
# yarn
yarn add typeorm reflect-metadata
# pnpm
pnpm add typeorm reflect-metadata
reflect-metadataはデコレータの動作に必要なので、一緒にインストールします。
データベースドライバーのインストール
使用するデータベースに応じてドライバーも追加します。
# PostgreSQL
npm install pg
# MySQL / MariaDB
npm install mysql2
# SQLite
npm install better-sqlite3
# MongoDB
npm install mongodb
# MS SQL Server
npm install mssql
TypeScript設定
tsconfig.jsonに以下を追加:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
デコレータを使うために必須の設定です。
データソース設定
src/data-source.tsを作成:
import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";
import { Post } from "./entity/Post";
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "password",
database: "myapp",
synchronize: true, // 開発時のみtrue
logging: true,
entities: [User, Post],
migrations: ["src/migration/*.ts"],
subscribers: [],
});
基本的な使い方
エンティティ定義
src/entity/User.tsを作成:
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from "typeorm";
import { Post } from "./Post";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 255 })
name: string;
@Column({ unique: true })
email: string;
@Column({ nullable: true })
age: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}
src/entity/Post.tsを作成:
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
} from "typeorm";
import { User } from "./User";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column("text")
content: string;
@CreateDateColumn()
createdAt: Date;
@ManyToOne(() => User, (user) => user.posts)
author: User;
}
デコレータを使ってテーブル定義できるのが直感的で良い。
データソースの初期化
import { AppDataSource } from "./data-source";
AppDataSource.initialize()
.then(() => {
console.log("データソース初期化完了");
})
.catch((error) => {
console.error("データソース初期化エラー:", error);
});
CRUD操作(DataMapperパターン)
import { AppDataSource } from "./data-source";
import { User } from "./entity/User";
const userRepository = AppDataSource.getRepository(User);
// INSERT
const newUser = new User();
newUser.name = "山田太郎";
newUser.email = "taro@example.com";
newUser.age = 30;
await userRepository.save(newUser);
// SELECT
const allUsers = await userRepository.find();
// WHERE句
const adults = await userRepository.find({
where: { age: MoreThan(18) },
});
// 複数条件
const targetUser = await userRepository.findOne({
where: {
name: "山田太郎",
age: MoreThan(25),
},
});
// ORDER BY + LIMIT
const recentUsers = await userRepository.find({
order: { createdAt: "DESC" },
take: 10,
});
// UPDATE
await userRepository.update(1, { age: 31 });
// DELETE
await userRepository.delete(1);
CRUD操作(ActiveRecordパターン)
エンティティを継承してActiveRecordスタイルにすることもできます。
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
}
// 使い方
const user = new User();
user.name = "山田太郎";
user.email = "taro@example.com";
await user.save();
// 検索
const users = await User.find();
const user = await User.findOneBy({ id: 1 });
シンプルなアプリならこっちの方が書きやすい。
リレーションの取得
// リレーションを含めて取得
const usersWithPosts = await userRepository.find({
relations: {
posts: true,
},
});
// 特定のユーザーの投稿を取得
const userWithPosts = await userRepository.findOne({
where: { id: 1 },
relations: ["posts"],
});
QueryBuilder
複雑なクエリはQueryBuilderで書けます。
// 基本的なQueryBuilder
const users = await userRepository
.createQueryBuilder("user")
.where("user.age > :age", { age: 18 })
.orderBy("user.name", "ASC")
.getMany();
// JOINとページネーション
const result = await userRepository
.createQueryBuilder("user")
.leftJoinAndSelect("user.posts", "post")
.where("user.age > :age", { age: 18 })
.skip(0)
.take(10)
.getManyAndCount();
const [users, total] = result;
// サブクエリ
const users = await userRepository
.createQueryBuilder("user")
.where((qb) => {
const subQuery = qb
.subQuery()
.select("post.authorId")
.from(Post, "post")
.where("post.title LIKE :title")
.getQuery();
return "user.id IN " + subQuery;
})
.setParameter("title", "%TypeORM%")
.getMany();
SQLに近い感覚で書けるのが良い。
マイグレーション
# マイグレーションファイル生成
npx typeorm migration:generate src/migration/CreateUserTable -d src/data-source.ts
# マイグレーション実行
npx typeorm migration:run -d src/data-source.ts
# マイグレーションの取り消し
npx typeorm migration:revert -d src/data-source.ts
# スキーマの同期(開発用)
npx typeorm schema:sync -d src/data-source.ts
実践的なユースケース
NestJSとの統合
TypeORMはNestJSとの相性が抜群です。
app.module.ts:
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./entities/user.entity";
import { UsersModule } from "./users/users.module";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "password",
database: "myapp",
entities: [User],
synchronize: true,
}),
UsersModule,
],
})
export class AppModule {}
users.service.ts:
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "./user.entity";
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}
async create(name: string, email: string): Promise<User> {
const user = this.usersRepository.create({ name, email });
return this.usersRepository.save(user);
}
async remove(id: number): Promise<void> {
await this.usersRepository.delete(id);
}
}
NestJSの公式ドキュメントでも推奨されているORMです。
トランザクション
await AppDataSource.transaction(async (transactionalEntityManager) => {
const user = new User();
user.name = "新規ユーザー";
user.email = "new@example.com";
await transactionalEntityManager.save(user);
const post = new Post();
post.title = "最初の投稿";
post.content = "こんにちは!";
post.author = user;
await transactionalEntityManager.save(post);
});
ソフトデリート
import { Entity, DeleteDateColumn } from "typeorm";
@Entity()
export class User {
// ... 他のカラム
@DeleteDateColumn()
deletedAt: Date;
}
// 論理削除
await userRepository.softDelete(1);
// 削除済みを含めて取得
const allUsers = await userRepository.find({
withDeleted: true,
});
// 復元
await userRepository.restore(1);
Drizzle ORM / Prismaとの比較
正直な比較をしておきます。
| 観点 | TypeORM | Drizzle ORM | Prisma |
|---|---|---|---|
| 歴史 | 2016年〜 | 2022年〜 | 2019年〜 |
| 設計パターン | 両方対応 | DataMapper | DataMapper |
| 学習コスト | 中程度 | 低い | 中程度 |
| 型推論 | 良い | 優秀 | 優秀 |
| バンドルサイズ | 大きめ | 7.4kb | 大きめ |
| NestJS統合 | 公式対応 | コミュニティ | 公式対応 |
| MongoDB対応 | あり | なし | あり |
| 成熟度 | 高い | 成長中 | 高い |
個人的には、NestJSを使うならTypeORM、エッジランタイムで軽量に動かすならDrizzle ORM、Prisma Studioが便利だと思うならPrismaという使い分けをしています。
TypeORMの強みは、長年の実績と安定性。エンタープライズ向けの大規模プロジェクトで採用されている事例が多い。
まとめ
TypeORMを導入して感じた変化:
- 柔軟性: DataMapperとActiveRecordの選択肢がある
- 安定性: 長年の実績で信頼できる
- DB対応: 10種類以上のデータベースに対応
- エコシステム: NestJSとの相性が抜群
- マイグレーション: 自動生成で時短になる
Node.jsでエンタープライズ向けのバックエンドを書くなら、TypeORMは間違いなく検討すべき選択肢ですね。
特に「NestJSでしっかりしたAPIを作りたい」という人には、TypeORMがハマると思います。公式でサポートされているという安心感がある。
これ、意外とデコレータベースの記法が好みで使っている人が多い印象です。「エンティティの定義がクラスっぽく書けて読みやすい」というニーズに応えてくれる。