---
title: "TypeORMとは？1.1系の使い方｜エンティティ・CRUD・リレーション・マイグレーションを実行して解説"
url: "https://www.issoh.co.jp/tech/details/7599/"
published: 2025-07-08
updated: 2026-10-20
categories: ["データベース"]
publisher: "株式会社一創"
---

# TypeORMとは？1.1系の使い方｜エンティティ・CRUD・リレーション・マイグレーションを実行して解説

TypeORMは、TypeScriptのクラスとデコレーターでテーブルを定義し、SQLを書かずにデータベースを読み書きできるNode.js向けのORM（オブジェクト関係マッピング）ライブラリです。2026年5月19日に1.0が公開され、長く非推奨だった `Connection` 系のAPIや `sqlite3` ドライバが削除されました。2026年10月20日時点のnpmの最新版は1.1.2（10月8日公開）で、各版の変更内容は[GitHubのリリース一覧](https://github.com/typeorm/typeorm/releases)で確認できます。

この記事では、TypeORM 1.1.2を実際にインストールし、エンティティ定義、保存・更新・削除、リレーション、マイグレーション、QueryBuilder、トランザクションまでを動かした手順を、そのときのコードと発行されたSQLつきで説明します。検証環境はNode.js v26.5.0、TypeScript 7.0.2、better-sqlite3 12.11.1です。

## まとめ：TypeORMの位置づけと1.x系で押さえる要点

- **何をするものか**：TypeScriptのクラスをテーブルに対応させるORMです。Data MapperとActive Recordの両方の書き方に対応しています。
- **最新版**：npmのlatestは1.1.2です。0.3系は `legacy` タグで0.3.31が公開されています。
- **開発状況**：2024年末にメンテナが交代し、2025年は575件のプルリクエストがマージされました。「開発が止まっている」という以前の評判は当てはまりません。
- **1.0の破壊的変更**：Node.js 20以上が必須になり、`createConnection` などのグローバル関数、`sqlite3` ドライバ、`TYPEORM_*` 環境変数による設定が削除されました。where条件に `null` や `undefined` を渡すと例外が出ます。
- **保存のメソッド**：主キー付きの `save()` は既存行を探すSELECTを先に発行し、配列の `save()` は1行ずつINSERTします。新規の大量データは `insert()`、一意キーで「あれば更新」なら `upsert()` を使います。
- **本番のスキーマ変更**：`synchronize: true` は使わず、マイグレーションを生成して適用します。

## TypeORMの定義と特徴

TypeORMはMITライセンスのオープンソースで、GitHubのスター数は2026年10月20日にGitHub APIで取得した値で36,662です。クラスに `@Entity()` を付けるとテーブル、プロパティに `@Column()` を付けるとカラムになります。取得結果はそのクラスのインスタンスとして返るため、エディタの補完と型チェックがそのまま効きます。ORMという仕組み全般は[ORMの仕組みとメリット・デメリットの解説](/tech/details/3755/)で扱っているので、ここではTypeORMならではの特徴に絞ります。

### Data MapperとActive Recordの両対応

Data Mapper方式では、エンティティはデータを持つだけのクラスにして、保存や検索は `dataSource.getRepository(User)` で取得したリポジトリから呼びます。Active Record方式では、エンティティが `BaseEntity` を継承し、クラス自身が操作メソッドを持ちます。

```
@Entity()
export class Tag extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number

    @Column({ unique: true })
    name: string
}

// TagをDataSourceのentitiesに登録し、initialize()とマイグレーションを済ませたあと、async関数の中で実行する
await Tag.create({ name: "orm" }).save()
const found = await Tag.findOneBy({ name: "orm" })
```

このコードは `INSERT INTO "tag"` と `SELECT ... WHERE "Tag"."name" = ? LIMIT 1` を発行し、`found` は `Tag` のインスタンスとして返りました。公式ドキュメントは、Data Mapperは大規模なアプリの保守性に、Active Recordは小規模なアプリの簡潔さに向くと説明しています。テストでリポジトリを差し替えたい場合や、NestJSで依存性注入を使う場合はData Mapperを選びます。この記事の以降のコードもData Mapperで書いています。

### 対応データベースとドライバ

1.1.2の package.json では、次のドライバがオプションの依存（peerDependencies）として宣言されています。使うデータベースのドライバだけを追加でインストールします。

| データベース        | DataSourceのtype | ドライバと対応版                   |
| ------------- | --------------- | -------------------------- |
| PostgreSQL    | postgres        | pg ^8.5.1                  |
| MySQL・MariaDB | mysql           | mysql2 ^3.15.3             |
| SQLite        | better-sqlite3  | better-sqlite3 ^12.0.0     |
| SQL Server    | mssql           | mssql ^12.0.0              |
| Oracle        | oracle          | oracledb ^6.3.0 または ^7.0.0 |
| MongoDB       | mongodb         | mongodb ^7.0.0             |

このほかSAP HANA、Google Cloud Spanner、ブラウザ向けのsql.jsにも対応しています。MySQLの旧クライアント `mysql` は1.0で対象外になり、`mysql2` だけが使えます。

## TypeORMの開発体制と1.0までの経緯

TypeORMは2016年にUmed Khudoiberdiev氏が作ったライブラリです。2024年10月1日の公式ブログ「[The Future of TypeORM](https://typeorm.io/blog/future-of-typeorm)」は、ここ数年で保守が大きく滞り、将来への不安が広がっていたと認めています。公式の[Maintainersページ](https://typeorm.io/maintainers)によると、2024年末に保守がDavid Höck氏とMichael Bromley氏へ引き継がれ、現在は13人のメンテナが名前を連ねています。

[1.0の公開告知](https://typeorm.io/blog/typeorm-1-0)には、引き継ぎ後の数字が載っています。2025年はパッチ版を8回出し、575件のプルリクエストをマージし（前年は63件）、2,300件を超えるissueを閉じました。1.0の開発には363コミット、53人が関わっています。開発資金はOpenCollectiveのスポンサーで集めています。

npmの公開履歴を見ても、1.0以降は1.1.0（7月13日）、1.1.1（9月1日）、1.1.2（10月8日）と1〜2か月ごとに正式版が出ており、nightly版は2026年10月1日から20日まで毎日公開されていました。「保守されているか」への2026年10月時点の答えは「保守されている」です。

## TypeORM 1.0で変わった点と0.3系からの移行

1.0は、0.3系で非推奨になっていたAPIをまとめて削除したリリースです。[公式の移行ガイド（Upgrading from 0.3 to 1.0）](https://typeorm.io/docs/releases/1.0/upgrading-from-0.3)から、影響を受けやすいものを抜き出しました。

| 0.3系の書き方                              | 1.x系での扱い                           |
| ------------------------------------- | ---------------------------------- |
| Node.js 16・18                         | 非対応（Node.js 20以上）                  |
| createConnection・getConnection        | 削除。new DataSource()を使う             |
| Connection・ConnectionOptions型         | 削除。DataSource・DataSourceOptions    |
| TYPEORM\_URLなどの環境変数、ormconfig.env     | 削除。TS・JSの設定ファイルで書く                 |
| type: “sqlite”                        | 削除。type: “better-sqlite3”          |
| findByIds(\[1, 2, 3\])                | 削除。findBy({ id: In(\[1, 2, 3\]) }) |
| exist()                               | exists()に改名                        |
| @EntityRepository・getCustomRepository | 削除。Repository.extend()             |
| findのjoinオプション                        | 削除。relationsかQueryBuilder          |
| 文字列配列のselect・relations                | 削除。オブジェクト形式で書く                     |

TypeORMは `.env` ファイルを自動で読み込まなくなり、`dotenv` への依存もなくなりました。接続情報を環境変数から渡す場合は、設定ファイルの中で `process.env` を自分で参照します。

### where条件のnull・undefinedが例外になる変更

移行ガイドが「実行時に既存のアプリを壊しうる大きな変更」と明記しているのが、`invalidWhereValuesBehavior` の既定値の変更です。0.3系では `findBy({ id: undefined })` のようにundefinedを渡すと条件が黙って無視され、全件が返っていました。1.x系ではnullとundefinedのどちらも例外になります。1.1.2で `findBy({ nickname: undefined })` と `findBy({ nickname: null })` を実行すると、次の出力になりました。

```
TypeORMError Undefined value encountered in property 'User.nickname' of a where condition. Set 'invalidWhereValuesBehavior.undefined' to 'ignore' in connection options to skip properties with undefined values.
TypeORMError Null value encountered in property 'User.nickname' of a where condition. To match with SQL NULL, the IsNull() operator must be used. Set 'invalidWhereValuesBehavior.null' to 'ignore' or 'sql-null' in connection options to skip or handle null values.
```

NULLの行を探すときは `IsNull()` 演算子を使います。検索フォームの未入力項目をそのままwhereに渡しているコードは、この変更で例外を出すようになります。その項目で絞り込まない場合は、条件を組み立てる側で項目をオブジェクトから除いてください。未入力を `IsNull()` に置き換えると、NULLの行だけを探す別の検索になります。どうしても旧挙動に戻す場合は、DataSourceに次の設定を加えます。

```
new DataSource({
    // ...
    invalidWhereValuesBehavior: {
        null: "ignore",
        undefined: "ignore",
    },
})
```

この設定の対象はfind系メソッド、リポジトリとEntityManagerの更新系メソッド、`setFindOptions()` です。QueryBuilderの `.where()` には影響しません。また1.1.0からは、`update()` や `delete()` に空の条件を渡すと拒否されます。1.1.2でも `update({}, { title: "x" })` は `Empty criteria(s) are not allowed for the update method.`、`delete({})` は同じ文言の `delete method` で止まりました。条件の渡し忘れによる全件更新・全件削除を防ぐための変更です。

### codemodによる自動移行とNestJSの対応版

公式の `@typeorm/codemod` を使うと、importの書き換え、APIの置き換え、findオプションの書式変更、依存パッケージの更新を自動で行えます。`--dry` を付けると、ファイルを書き換えずに変更内容だけを確認できます。自動化できない箇所には `TODO` コメントが残ります。

```
npx @typeorm/codemod v1 src/
```

NestJSで `@nestjs/typeorm` を使っている場合、v10とv11.0.0は起動時に削除済みの `Connection` クラスを登録しようとして落ちます。移行ガイドはv11.0.1以降への更新を求めています。2026年10月20日時点の最新版は12.0.2（9月28日公開）で、peerDependenciesは `typeorm: ^0.3.0 || ^1.0.0-dev`、`@nestjs/core: ^10.0.0 || ^11.0.0 || ^12.0.0` です。

## TypeORM 1.1.2のインストールと初期設定

1.1.2が求めるNode.jsは `^20.19.0 || ^22.13.0 || >=24.11.0` です。Node.jsの版の選び方は[Node.jsの仕組みと版の選び方](/tech/details/17433/)で整理しています。

### パッケージの導入

本体、デコレーターのメタデータを扱う `reflect-metadata`、使うデータベースのドライバを入れます。検証ではSQLiteを使いました。

```
npm install typeorm@1.1.2 reflect-metadata@0.2.2 better-sqlite3@12.11.1
npm install -D typescript@7.0.2 @types/node@22
```

npm 11.17.0では、better-sqlite3のインストール時に、`allowScripts` で承認されていないインストールスクリプトがあるという `npm warn allow-scripts` の警告が出ました。この警告が出た状態でもネイティブモジュールはビルドされ、ドライバを読み込めました。警告を消すには `npm approve-scripts better-sqlite3` で承認し、package.json の `allowScripts` に記録します。

エディタで `Cannot find module 'typeorm' or its corresponding type declarations.` が出る場合は、typeormが `node_modules` に入っているか、tsconfig.jsonの `module` の設定とimportの書き方が合っているかを確認します。

### tsconfig.jsonの必須オプション

デコレーターを使うため、`experimentalDecorators` と `emitDecoratorMetadata` を有効にします。TypeScript 7.0.2でコンパイルするには、TypeORMの公式手順に載っていない設定が3つ必要でした。1つ目は `rootDir` で、指定しないとエラーTS5011で止まります。2つ目は `types: ["node"]` で、指定しないとTypeORMの型定義の中で `Buffer` や `tls` が見つからないというエラーが出ます。3つ目は `lib` の `ESNext.Disposable` で、`AsyncDisposable` が見つからないというエラーを解消します。`skipLibCheck` は使わず、次の設定でコンパイルを通しました。

```
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "commonjs",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "strictPropertyInitialization": false,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "lib": ["ES2023", "ESNext.Disposable"],
    "types": ["node"]
  },
  "include": ["src"]
}
```

`strictPropertyInitialization` を無効にしているのは、エンティティのプロパティに初期値を書かないためです。有効のまま使う場合は、各プロパティに `!` を付けます。

### DataSourceの設定ファイル

接続設定は `DataSource` のインスタンスとして書き、アプリとCLIの両方から読み込みます。

```
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entities/User"
import { Post } from "./entities/Post"

export const AppDataSource = new DataSource({
    type: "better-sqlite3",
    database: "app.sqlite",
    entities: [User, Post],
    migrations: ["dist/migrations/*.js"],
    synchronize: false,
    logging: ["query", "error"],
})
```

ファイルの配置は `src/data-source.ts`、`src/entities/User.ts`、`src/entities/Post.ts` です。`logging` に `"query"` を入れると、発行されたSQLがそのまま出力されます。以降の章のSQLは、この設定で記録したものです。

## エンティティ定義で使うデコレーター

ユーザーと投稿の2テーブルを定義します。

```
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from "typeorm"
import { Post } from "./Post"

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

    @Column({ length: 100, unique: true })
    email: string

    @Column({ type: "varchar", nullable: true })
    nickname: string | null

    @CreateDateColumn()
    createdAt: Date

    @OneToMany(() => Post, (post) => post.author)
    posts: Post[]
}
```

```
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"
import { User } from "./User"

@Entity()
export class Post {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    title: string

    @Column({ default: false })
    published: boolean

    @ManyToOne(() => User, (user) => user.posts, { nullable: false })
    author: User
}
```

ここで使ったデコレーターと、マイグレーションで実際に生成されたDDLの対応は次のとおりです。

| デコレーターとオプション                                 | SQLiteで生成された定義                              |
| -------------------------------------------- | ------------------------------------------- |
| @PrimaryGeneratedColumn()                    | integer PRIMARY KEY AUTOINCREMENT NOT NULL  |
| @Column({ length: 100, unique: true })       | varchar(100) NOT NULL ＋ UNIQUE制約            |
| @Column({ type: “varchar”, nullable: true }) | varchar（NULL許可）                             |
| @Column({ default: false })                  | boolean NOT NULL DEFAULT (0)                |
| @CreateDateColumn()                          | datetime NOT NULL DEFAULT (datetime(‘now’)) |
| @ManyToOne(…, { nullable: false })           | authorId integer NOT NULL ＋ 外部キー            |

`@Column()` は、オプションを省くとNOT NULLになります。NULLを許すカラムは `nullable: true` を明示してください。`string | null` のようなユニオン型は、デコレーターのメタデータから列の型を推定できません。そのため `type` も指定しています。`type` を省くと、初期化時に `DataTypeNotSupportedError`（`Data type "Object" in "Bad.displayName" is not supported by "better-sqlite3" database.`）で止まりました。

エンティティはDataSourceの `entities` に登録します。登録を忘れたクラスでクエリを実行すると、`EntityMetadataNotFoundError`（`No metadata for "Tag2" was found.`）が出ます。`entities` にglobパターンで `.ts` を指定したまま、コンパイル後の `.js` で起動した場合もこのエラーになります。NestJSでAPIの入出力にエンティティを直接使わず、DTOを分けて定義する考え方は[DTOの意味とEntityとの違い](/tech/details/6208/)で扱っています。

## save・insert・upsert・update・deleteの使い分け

リポジトリの書き込み系メソッドは、名前が似ていても発行するSQLの本数と、重複時の振る舞いが違います。後述の `main()` の中で `const repo = AppDataSource.getRepository(User)` と `const posts = AppDataSource.getRepository(Post)` を用意し、1.1.2とbetter-sqlite3で実行したときのSQLを並べました。`id` と `createdAt` はDBが値を決める列なので、INSERTのあとにその値を読むSELECTが続きます。

| メソッド          | 発行されたSQL                                | 重複キーのとき          |
| ------------- | --------------------------------------- | ---------------- |
| save（主キーなし）   | INSERT → 生成列を読むSELECT                   | 例外               |
| save（配列2件）    | 1件ずつINSERT＋SELECT（計4本）                  | 例外               |
| save（主キー付き）   | SELECT → 変わった列だけUPDATE                  | –                |
| save（変更なし）    | SELECTのみ                                | –                |
| insert（配列2件）  | 複数行INSERT 1本 → SELECT 1本                | QueryFailedError |
| upsert        | INSERT … ON CONFLICT DO UPDATE → SELECT | 更新される            |
| update・delete | 条件つきUPDATE・DELETE 1本                    | –                |

### saveの事前SELECTと配列保存の発行本数

[公式のRepository API](https://typeorm.io/docs/working-with-entity-manager/repository-api)は、`save()` を「DBに存在すれば更新、無ければ挿入」と説明し、undefinedのプロパティは更新対象から外れると書いています。主キーだけを付けた部分オブジェクトを渡すと、次のSQLが流れました。

```
await repo.save({ id: 2, nickname: "cc" })

SELECT "User"."id" AS "User_id", "User"."email" AS "User_email", ... FROM "user" "User" WHERE "User"."id" = 2
BEGIN TRANSACTION
UPDATE "user" SET "nickname" = ? WHERE "id" = 2 -- PARAMETERS: ["cc"]
COMMIT
```

既存行を読み、差分のある `nickname` だけを更新しています。読み込んだエンティティを何も変えずに `save()` に渡したときはSELECTだけが流れ、UPDATEは発行されませんでした。主キーを持たない新規の保存では、この事前SELECTは出ません。

本数の差が出るのは配列です。2件の配列を `save()` に渡すと、トランザクションの中で1件ずつINSERTと生成列のSELECTを繰り返し、計4本になりました。同じ2件を `insert()` に渡すと、`VALUES (...), (...)` の複数行INSERT 1本と、2件の `id` をまとめて読むSELECT 1本で済みました。件数が増えるほど往復の差は開くので、新規と分かっている大量データは `insert()` に配列で渡します。主キーと変更列が分かっている更新は、`update(条件, 値)` で1本のUPDATEにできます。

### insertとupsertの重複キーでの違い

メールアドレスに一意制約がある `User` へ、既にある `c@example.com` を `insert()` すると、`QueryFailedError SqliteError: UNIQUE constraint failed: user.email` で失敗しました。同じ値を `upsert()` に渡すと、衝突したら更新する書き込みが1本で発行され、そのあと生成列を読むSELECTが続きました。書き込み部分は次のとおりです。

```
await repo.upsert({ email: "c@example.com", nickname: "upserted" }, ["email"])

INSERT INTO "user"("id", "email", "nickname", "createdAt") VALUES (NULL, ?, ?, datetime('now')) ON CONFLICT ( "email" ) DO UPDATE SET "email" = EXCLUDED."email", "nickname" = EXCLUDED."nickname"
```

第2引数には、衝突を判定する一意制約の列を渡します。公式ドキュメントが `upsert()` の対応ドライバとして挙げるのはAurora Data API、CockroachDB、MySQL、PostgreSQL、SQLiteで、SQL ServerとOracleは含まれません。SQL ServerやOracleでは、存在を確かめてから `insert()` か `update()` を呼ぶ形になりますが、これだけでは同時に動く2つの処理がどちらも「無い」と判断して、片方が一意制約違反になります。一意制約違反を捕まえて `update()` でやり直す処理を入れるか、各DBのMERGE文やロックの仕組みをQueryBuilderや生SQLで使ってください。

### softDeleteによる論理削除と復元

行を消さずに削除済みとして扱うには、エンティティに `@DeleteDateColumn()` を足します。`Post` に `deletedAt: Date | null` を追加して実行した結果です。

```
await posts.softDelete({ title: "下書き" })
// UPDATE "post" SET "deletedAt" = CURRENT_TIMESTAMP WHERE "title" = ? AND "deletedAt" IS NULL

await posts.count()                       // 1（削除済みを除外）
await posts.count({ withDeleted: true })  // 2

await posts.restore({ title: "下書き" })
// UPDATE "post" SET "deletedAt" = NULL WHERE "title" = ? AND "deletedAt" IS NOT NULL
```

列を足したあとは、find系メソッドとQueryBuilderのSQLに `"deletedAt" IS NULL` が自動で付きました。`relations` で投稿を読み込むJOINの条件にも入ります。削除済みを含めて集計する管理画面では `withDeleted: true` を忘れると数が合いません。一意制約は削除済みの行にも効くので、論理削除したユーザーと同じメールアドレスでは再登録できない点にも注意してください。

## リレーションの設定方法

リレーションは、片方のエンティティだけにデコレーターを付ける単方向と、両方に付けて互いに参照できる双方向のどちらでも定義できます。1対1と多対1では外部キーを持つ側、多対多では `@JoinTable` を付けた側を「所有側（owner）」と呼びます。多対多の外部キーは、エンティティのテーブルではなく中間テーブルに作られます。

| 関係      | 所有側に付けるもの                | 反対側         |
| ------- | ------------------------ | ----------- |
| 多対1・1対多 | @ManyToOne（外部キーを自動で作成）   | @OneToMany  |
| 1対1     | @OneToOne ＋ @JoinColumn  | @OneToOne   |
| 多対多     | @ManyToMany ＋ @JoinTable | @ManyToMany |

### ManyToOneとOneToManyの対応

先ほどの `Post.author`（`@ManyToOne`）と `User.posts`（`@OneToMany`）は、第2引数で互いのプロパティを指し合っています。外部キー列 `authorId` は所有側の `post` テーブルに作られます。公式ドキュメントのとおり、`@OneToMany` は反対側の `@ManyToOne` がなければ使えません。逆に `@ManyToOne` だけを定義することはできます。`relations: { posts: true }` でユーザーを取得すると、次のように `LEFT JOIN` で投稿が読み込まれました（列リストは省略）。

```
SELECT ... FROM "user" "User" LEFT JOIN "post" "User__User_posts" ON "User__User_posts"."authorId"="User"."id" WHERE (("User"."nickname" IS NULL))
```

### nullable: falseでINNER JOINになる挙動

1.0からは、`nullable: false` を付けた `@ManyToOne` と、所有側の `@OneToOne` を `relations` で読み込むと `INNER JOIN` が使われます。1.1.2でも、`Post` を `relations: { author: true }` で取得したSQLは `INNER JOIN "user" "Post__Post_author"` でした。外部キーがNOT NULLで、参照先の行が必ず存在するなら結果は変わりません。ただし、実際のテーブルでは外部キーがNULL許可のままになっている場合や、参照先の無い行が残っている場合は、その行が結果から消えます。移行時は、エンティティの `nullable` とDBの実際の定義が一致しているかを確認してください。関連先に `@DeleteDateColumn` がある場合は例外で、`LEFT JOIN` のままです。

### OneToOneとManyToManyの所有側

1対1では、`@JoinColumn()` を外部キーを持つ側だけに付けます。公式ドキュメントはこれを必須とし、両側に付けてはいけないと明記しています。多対多では `@JoinTable()` が必須で、これも片側だけに付けます。付けた側のエンティティ名を元に、中間テーブル（例：`question_categories_category`）が作られます。中間テーブルに作成日時などの列を持たせたい場合は、`@ManyToMany` ではなく中間エンティティを定義し、`@ManyToOne` を2本張る設計にします。

### EagerとLazyの使い分け

`eager: true` を付けたリレーションは、find系メソッドで自動的に読み込まれます。ただし公式ドキュメントのとおり、QueryBuilderでは無効で、`leftJoinAndSelect` を書く必要があります。また、eagerはリレーションの片側にしか付けられません。Lazyリレーションは、プロパティの型を `Promise<Post[]>` にし、`await` したときに別クエリで読み込む方式です。一覧画面でLazyを使うと、行ごとにクエリが発行されるN+1問題が起きます。発行本数の実測とORM別の対策は[N+1問題の発行クエリ数の実測と対策](/tech/details/4262/)にまとめました。一覧では `relations` かJOINで一度に取得し、eagerとLazyは使う場所を限定します。

## マイグレーションによるスキーマ管理

### synchronizeを本番で使わない理由

`synchronize: true` は、起動のたびにエンティティとDBの差分を自動で反映します。[公式のDataSourceオプションの説明](https://typeorm.io/docs/data-source/data-source-options)には「本番では使わないこと。本番データを失う恐れがある」とあります。SQLiteで外部キーを追加する差分では、TypeORMは `temporary_post` テーブルを作り、データを移してから元のテーブルを `DROP` していました。どのような変更SQLが流れるかを事前に確認できないまま本番のテーブルが作り直されるため、開発初期だけ有効にし、共有する環境ではマイグレーションに切り替えます。

### generate・run・revertのコマンド

TypeORMのCLIは `typeorm` コマンドで、`-d` にDataSourceのファイルを渡します。検証では、コンパイル済みのJavaScriptに対してCLIを実行しました。この方法ならts-nodeを入れる必要がありません。

```
npx tsc
npx typeorm migration:generate src/migrations/Init -d dist/data-source.js
npx tsc
npx typeorm migration:run -d dist/data-source.js
npx typeorm migration:show -d dist/data-source.js

# 取り消す場合だけ実行する（テーブルが消えるので、続けて使うなら再度runする）
npx typeorm migration:revert -d dist/data-source.js
```

`migration:generate` は、エンティティと現在のDBを比べて差分のSQLを `up`、元に戻すSQLを `down` に書いたファイルを作ります（今回は `src/migrations/1792499394858-Init.ts`）。差分が無いときは `No changes in database schema were found - cannot generate a migration.` と表示され、ファイルは作られません。空のマイグレーションを手で書く場合は `migration:create` を使います。`migration:show` は適用済みを `[X] 1 Init1792499394858` のように表示します。`migration:revert` は最後に適用した1件の `down` を実行します。

生成されたSQLは、コミット前に必ず読んでください。`nickname` を `displayName` に改名する差分では、SQLiteでは `temporary_user` を作って `INSERT INTO ... SELECT` で旧列の値を移し、元の `user` テーブルを `DROP` するSQLが生成されました。データは引き継がれますが、行数の多いテーブルでは作り直しに時間がかかり、その間は書き込みができません。本番環境では、アプリの起動時に `runMigrations()` を自動実行するより、デプロイの手順でCLIを1回だけ実行するほうが安全です。複数台が同時に起動しても、マイグレーションが二重に走りません。既存DBの移行計画や本番での切り替え手順を外部に相談したい場合は、[データベース設計・移行支援](/service/system/database/)で対応しています。

## データ取得のfind APIとQueryBuilder

ここからのコード片は、検証用の `src/main.ts` の `main()` 関数の中で実行したものです。出力は、`app.sqlite` が無い状態から初回に実行したときのものです（2回目は同じメールアドレスの保存が一意制約で止まるので、ファイルを消してから実行します）。関数の冒頭では、DataSourceを初期化してマイグレーションを適用し、`a@example.com` のユーザーと投稿2件をトランザクションで保存しています。

```
import { IsNull } from "typeorm"
import { AppDataSource } from "./data-source"
import { User } from "./entities/User"
import { Post } from "./entities/Post"

async function main() {
    await AppDataSource.initialize()
    await AppDataSource.runMigrations()

    await AppDataSource.transaction(async (manager) => {
        const user = await manager.save(User, { email: "a@example.com", nickname: null })
        await manager.save(Post, [
            { title: "TypeORM 1.1を試す", author: user },
            { title: "下書き", author: user },
        ])
    })
    // 以降のコード片はここに続けて書く
    await AppDataSource.destroy()
}
main()
```

単純な条件の検索は、リポジトリの `find` 系メソッドで書きます。1.x系では、`select`、`relations`、`order` をオブジェクト形式で指定します。

```
const users = await AppDataSource.getRepository(User).find({
    where: { nickname: IsNull() },
    relations: { posts: true },
})
```

集計や複雑なJOIN、取得する列の絞り込みはQueryBuilderで書きます。値は `:published` のような名前付きパラメータで渡し、文字列連結でSQLに埋め込まないようにします。

```
const rows = await AppDataSource.getRepository(Post)
    .createQueryBuilder("post")
    .innerJoin("post.author", "author")
    .select(["post.title AS title", "author.email AS email"])
    .where("post.published = :published", { published: false })
    .orderBy("post.id", "ASC")
    .getRawMany()
```

発行されたSQLと、返ってきた結果は次のとおりです。

```
SELECT "post"."title" AS title, "author"."email" AS email FROM "post" "post" INNER JOIN "user" "author" ON "author"."id"="post"."authorId" WHERE "post"."published" = ? ORDER BY "post"."id" ASC -- PARAMETERS: [0]
[
  { title: 'TypeORM 1.1を試す', email: 'a@example.com' },
  { title: '下書き', email: 'a@example.com' }
]
```

`getRawMany()` は、エンティティではなく列名をキーにしたオブジェクトを返します。エンティティとして受け取りたい場合は、`innerJoinAndSelect` と `getMany()` を組み合わせ、独自の別名を付けた `select()` は外します。別名付きの `select()` を残したまま `getMany()` を呼ぶと、0件の配列が返りました。`select()` を外すと、`author` を含む `Post` のインスタンスが2件返りました。1.0では、`orderBy` に渡した値の検証と、スキーマ操作での識別子のエスケープが全ドライバに入りました。いずれもSQLインジェクション対策としてリリースノートに記載されています。

## トランザクションの書き方

### transaction()のコールバックとロールバック

`dataSource.transaction()` に渡したコールバックの中では、引数の `manager` を通して操作します。コールバックが例外を投げると、それまでの変更はすべてロールバックされます。メールアドレスが重複するユーザーを2件目に保存させて、ロールバックを確認しました。

```
try {
    await AppDataSource.transaction(async (manager) => {
        await manager.save(User, { email: "b@example.com" })
        await manager.save(User, { email: "a@example.com" })
    })
} catch (e) {
    console.log("rollback:", await AppDataSource.getRepository(User).count())
}
```

このコードを実行すると、`UNIQUE constraint failed: user.email` で2件目の `INSERT` が失敗し、`ROLLBACK` が発行されました。1件目の `b@example.com` も残らず、ユーザー数は1件のままでした。コールバック内で `AppDataSource.getRepository()` を直接使うと、その操作がトランザクションに参加することは保証されません。公式ドキュメントの指示どおり、必ず `manager` を使ってください。分離レベルは、1.0からDataSourceの `isolationLevel` で全ドライバの既定値を設定できます。

### await usingのQueryRunnerが例外時にコミットする挙動

1.0から、`QueryRunner` は `await using` 構文（TypeScript 5.2以降）に対応し、スコープを抜けると自動で解放されます。1.0の告知は、接続を返し忘れる不具合を減らす機能として紹介しています。ただし、解放処理はトランザクションをロールバックしません。1.1.2の `BaseQueryRunner` の `[Symbol.asyncDispose]()` は、トランザクションが開いていれば `commitTransaction()` を呼んでから `release()` します。途中で例外を投げると、次のようになりました。

```
try {
    await using qr = AppDataSource.createQueryRunner()
    await qr.startTransaction()
    await qr.manager.save(User, { email: "dispose@example.com" })
    throw new Error("途中で失敗")
} catch (e) {
    // BEGIN TRANSACTION → INSERT → COMMIT の順に発行された
}
await AppDataSource.getRepository(User).countBy({ email: "dispose@example.com" })  // 1
```

例外で中断したのに、`INSERT` した行はコミットされて残りました。この挙動は[issue #12913](https://github.com/typeorm/typeorm/issues/12913)（2026年9月30日起票・10月20日時点でopen）で、解放時にロールバックを選べる設定の追加が提案されています。公式ドキュメントの `await using` の例はトランザクションを使っていないため、この点は書かれていません。

途中で失敗したら取り消したい処理には、`await using` と `startTransaction()` を組み合わせないでください。`dataSource.transaction()` を使うか、`QueryRunner` を手で扱う場合は `try` で `commitTransaction()`、`catch` で `rollbackTransaction()`、`finally` で `release()` を呼ぶ従来の書き方にします。`await using` は、読み取りだけや、1文ずつ確定してよい処理での解放漏れ防止に限って使います。

## よくあるエラーと対処

- **No metadata for “X” was found**：エンティティが `entities` に登録されていない、またはglobの拡張子が実行環境（`.ts`か`.js`か）と合っていません。
- **Undefined value encountered in property …**：where条件にundefinedが入っています。条件オブジェクトから項目を除くか、NULLを探すなら `IsNull()` を使います。
- **Empty criteria(s) are not allowed for the update method.**：`update()` や `delete()` の条件が空です。
- **UNIQUE constraint failed（QueryFailedError）**：`insert()` か新規の `save()` で一意制約に当たっています。あれば更新でよいなら `upsert()` に切り替えます。
- **createConnectionやgetConnectionが見つからない**：1.x系で削除されたAPIを呼んでいます。`new DataSource()` と `initialize()` に書き換えます。
- **sqlite3ドライバが読み込まれない**：1.x系では、`type: "better-sqlite3"` とbetter-sqlite3パッケージに切り替えます。オプションの `busyTimeout` は `timeout` に名前が変わりました。
- **No changes in database schema were found**：`migration:generate` で差分が無い状態です。エンティティの変更がビルド後のJSに反映されているか確認します。

NestJSの起動時に、`Connection` に関連するエラーで落ちる場合は、`@nestjs/typeorm` が11.0.1より古い版のままになっています。

## TypeORMを選ぶ条件と避けるべき場面

TypeORMが向いているのは、クラスとデコレーターでモデルを書く設計（NestJSなど）を採用している場合、SQLに近い書き方でJOINや集計を組みたい場合、PostgreSQLとOracleのように複数のDBを同じ書き方で扱いたい場合です。既に0.3系で動いているNestJSのアプリなら、別のORMへ書き換える前に、codemodで1.x系へ上げて残る `TODO` の量を確かめるところから始めます。

避けるべきなのは、型安全を最優先する場合です。`getRawMany()` の戻り値や、QueryBuilderに文字列で書いた列名は、コンパイル時に検査されません。スキーマファイルから型を生成するPrismaや、SQLに近い書き方で結果の型まで推論するDrizzleのほうが、列名の打ち間違いをコンパイル時に見つけられます。PrismaとTypeORMの比較は[TypeORMとPrismaの比較記事](/tech/details/4331/)で、Drizzleの特徴は[Drizzle ORMの使い方とv1.0 RCの変更点](/tech/details/7939/)で詳しく扱っています。

## よくある質問

### TypeORMとは何ですか？

TypeScriptのクラスとデコレーターでテーブルを定義し、リポジトリやQueryBuilderでDBを操作するNode.js向けのORMです。PostgreSQL、MySQL、SQLite、SQL Server、Oracle、MongoDBなどに対応しています。

### TypeORMの最新バージョンはいくつですか？

2026年10月20日時点で、npmのlatestは1.1.2（2026年10月8日公開）です。0.3系は `legacy` タグで0.3.31が公開されています。最新の版は `npm view typeorm dist-tags` で確認できます。

### 0.3系のコードは1.x系でそのまま動きますか？

非推奨APIを使っていなければ、多くはそのまま動きます。ただし、where条件のnull・undefinedが例外になる変更と、`nullable: false` のリレーションがINNER JOINになる変更は、コンパイルでは検出できず実行時に表面化します。`npx @typeorm/codemod v1 src/` で書き換えたうえで、テストを実行してください。

### NestJSでTypeORM 1.xを使うには何が必要ですか？

`@nestjs/typeorm` をv11.0.1以降に上げます。v10とv11.0.0は起動時に落ちます。NestJS 12と組み合わせる場合は、`@nestjs/core` の12系をpeerDependenciesに含む12.0.0以降を使います。環境変数から接続情報を読む設定は、[@nestjs/config（ConfigModule）の使い方](/tech/details/12632/)を参考にしてください。

### saveとinsertはどちらを使えばよいですか？

新規か既存か分からないエンティティを保存するなら `save()` です。新規と分かっている大量データは `insert()` に配列で渡します。検証では2件の保存が `save()` で4本、`insert()` で2本のSQLになりました。一意キーが重複したら更新したいときは `upsert()` を使います。

## 関連記事

- [TypeORMとPrismaを比較｜2026年版の違いと選び方（型安全・マイグレーション・パフォーマンス）](/tech/details/4331/)
- [ORMとは？O/Rマッパーの仕組み・メリットとデメリットをSQL比較と実行結果で解説](/tech/details/3755/)
- [Drizzle ORM（ドリズル）とは？使い方・Prismaとの比較・v1.0 RCの変更点【2026年9月】](/tech/details/7939/)
- [N+1問題とは？発行クエリ数の実測と検出方法・ORM別の対策](/tech/details/4262/)
- [DTOとは？意味・Entityとの違いとNestJSのValidationPipe設定【v12実測】](/tech/details/6208/)

---

出典: [TypeORMとは？1.1系の使い方｜エンティティ・CRUD・リレーション・マイグレーションを実行して解説](<https://www.issoh.co.jp/tech/details/7599/>)（株式会社一創）
