TypeORMは、TypeScriptのクラスとデコレーターでテーブルを定義し、SQLを書かずにデータベースを読み書きできるNode.js向けのORM(オブジェクト関係マッピング)ライブラリです。2026年5月19日に1.0が公開され、長く非推奨だった Connection 系のAPIや sqlite3 ドライバが削除されました。2026年9月時点のnpmの最新版は1.1.1で、各版の変更内容はGitHubのリリース一覧で確認できます。0.3系の情報をもとに書いたコードは、1.x系ではそのまま動かないことがあります。
この記事では、TypeORM 1.1.1を実際にインストールし、エンティティ定義、リレーション、マイグレーション、QueryBuilder、トランザクションまでを動かした手順を、そのときのコードと出力つきで説明します。検証環境は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.1(2026年9月1日公開)です。0.3系は
legacyタグで0.3.31まで保守されています。 - 1.0の破壊的変更:Node.js 20以上が必須になり、
createConnectionなどのグローバル関数、sqlite3ドライバ、TYPEORM_*環境変数による設定が削除されました。 - 実行時に効く変更:where条件に
nullやundefinedを渡すと、無視されずに例外が出るようになりました。null判定はIsNull()で書きます。 - 本番のスキーマ変更:
synchronize: trueは使わず、マイグレーションを生成して適用します。
以下、定義と特徴から順に、動かしたコードを交えて解説します。
TypeORMの定義と特徴
TypeORMはMITライセンスのオープンソースで、GitHubのスター数は2026年9月時点で約3万7,000です。クラスに @Entity() を付けるとテーブル、プロパティに @Column() を付けるとカラムになります。取得結果はそのクラスのインスタンスとして返るため、エディタの補完と型チェックがそのまま効きます。ORMという仕組み全般の説明は省き、ここではTypeORMならではの特徴に絞ります。
Data MapperとActive Recordの両対応
Data Mapper方式では、エンティティはデータを持つだけのクラスにして、保存や検索は dataSource.getRepository(User) で取得したリポジトリから呼びます。Active Record方式では、エンティティが BaseEntity を継承し、User.find() や user.save() のようにクラス自身が操作メソッドを持ちます。公式ドキュメントは、Data Mapperは大規模なアプリの保守性に、Active Recordは小規模なアプリの簡潔さに向くと説明しています。テストでリポジトリを差し替えたい場合や、NestJSで依存性注入を使う場合はData Mapperを選ぶのが無難です。この記事のコードもData Mapperで書いています。
対応データベースとドライバ
1.1.1の 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で変わった点と0.3系からの移行
1.0は、0.3系で非推奨になっていたAPIをまとめて削除したリリースです。公式の移行ガイド(Upgrading from 0.3 to 1.0)から、影響を受けやすいものを抜き出しました。
| 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のどちらも例外になります。検証コードで findBy({ nickname: undefined }) を実行すると、次の出力になりました。
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.
NULLの行を探すときは IsNull() 演算子を使います。検索フォームの未入力項目をそのままwhereに渡しているコードは、この変更で例外を出すようになります。その項目で絞り込まない場合は、条件を組み立てる側で項目をオブジェクトから除いてください。未入力を IsNull() に置き換えると、NULLの行だけを探す別の検索になります。どうしても旧挙動に戻す場合は、DataSourceに次の設定を加えます。
new DataSource({
// ...
invalidWhereValuesBehavior: {
null: "ignore",
undefined: "ignore",
},
})
この設定の対象はfind系メソッド、リポジトリとEntityManagerの更新系メソッド、setFindOptions() です。QueryBuilderの .where() には影響しません。また1.1.0からは、update() や delete() に空の条件を渡すと拒否されます。検証では update({}, { title: "x" }) が Empty criteria(s) are not allowed for the update 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年9月時点の最新版は12.0.1で、peerDependenciesは typeorm: ^0.3.0 || ^1.0.0-dev です。
TypeORM 1.1.1のインストールと初期設定
1.1.1が求めるNode.jsは ^20.19.0 || ^22.13.0 || >=24.11.0 です。Node.jsの版の選び方はNode.jsの仕組みと版の選び方で整理しています。
パッケージの導入
本体、デコレーターのメタデータを扱う reflect-metadata、使うデータベースのドライバを入れます。検証ではSQLiteを使いました。
npm install [email protected] [email protected] [email protected]
npm install -D [email protected] @types/[email protected]
版は検証時のものに固定しています。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 "User.displayName" is not supported by "better-sqlite3" database.)で止まりました。
エンティティはDataSourceの entities に登録します。登録を忘れたクラスでクエリを実行すると、EntityMetadataNotFoundError が出ます(検証時のメッセージは No metadata for "Tag" was found.)。entities にglobパターンで .ts を指定したまま、コンパイル後の .js で起動した場合もこのエラーになります。NestJSでAPIの入出力にエンティティを直接使わず、DTOを分けて定義する考え方はNestJSにおけるDTOの基本で扱っています。
リレーションの設定方法
リレーションは、片方のエンティティだけにデコレーターを付ける単方向と、両方に付けて互いに参照できる双方向のどちらでも定義できます。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 が使われます。検証でも、Post を relations: { author: true } で取得したSQLは INNER JOIN でした。外部キーが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問題が起きやすくなります。一覧では relations かJOINで一度に取得し、eagerとLazyは使う場所を限定するのが安全です。
マイグレーションによるスキーマ管理
synchronizeを本番で使わない理由
synchronize: true は、起動のたびにエンティティとDBの差分を自動で反映します。公式のDataSourceオプションの説明には「本番では使わないこと。本番データを失う恐れがある」とあります。検証でも、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
npx typeorm migration:run -d dist/data-source.js
migration:generate は、エンティティと現在のDBを比べて差分のSQLを up、元に戻すSQLを down に書いたファイルを作ります(今回は src/migrations/1789637666312-Init.ts)。差分が無いときは No changes in database schema were found - cannot generate a migration. と表示され、ファイルは作られません。空のマイグレーションを手で書く場合は migration:create を使います。migration:show は適用済みを [X] で表示します。migration:revert は最後に適用した1件の down を実行します。
生成されたSQLは、コミット前に必ず読んでください。検証で nickname を displayName に改名したところ、SQLiteでは temporary_user を作って INSERT INTO ... SELECT で旧列の値を移し、元の user テーブルを DROP するSQLが生成されました。データは引き継がれますが、行数の多いテーブルでは作り直しに時間がかかり、その間は書き込みができません。本番環境では、アプリの起動時に runMigrations() を自動実行するより、デプロイの手順でCLIを1回だけ実行するほうが安全です。複数台が同時に起動しても、マイグレーションが二重に走りません。
データ取得のfind APIとQueryBuilder
ここからのコード片は、検証用の src/main.ts の main() 関数の中で実行したものです。関数の冒頭では、DataSourceを初期化してマイグレーションを適用し、[email protected] のユーザーと投稿2件をトランザクションで保存しています。関数の末尾では AppDataSource.destroy() で接続を閉じています。
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: "[email protected]", nickname: null })
await manager.save(Post, [
{ title: "TypeORM 1.1を試す", author: user },
{ title: "下書き", author: user },
])
})
npx tsc でコンパイルし、node dist/main.js で実行しました。単純な条件の検索は、リポジトリの 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: '[email protected]' },
{ title: '下書き', email: '[email protected]' }
]
getRawMany() は、エンティティではなく列名をキーにしたオブジェクトを返します。エンティティとして受け取りたい場合は、innerJoinAndSelect と getMany() を組み合わせ、独自の別名を付けた select() は外します。別名付きの select() を残したまま getMany() を呼ぶと、検証では0件の配列が返りました。select() を外すと、author を含む Post のインスタンスが返りました。1.0では、orderBy に渡した値の検証と、スキーマ操作での識別子のエスケープが全ドライバに入りました。いずれもSQLインジェクション対策としてリリースノートに記載されています。
トランザクションの書き方
dataSource.transaction() に渡したコールバックの中では、引数の manager を通して操作します。コールバックが例外を投げると、それまでの変更はすべてロールバックされます。検証では、メールアドレスが重複するユーザーを2件目に保存させて、ロールバックを確認しました。
try {
await AppDataSource.transaction(async (manager) => {
await manager.save(User, { email: "[email protected]" })
await manager.save(User, { email: "[email protected]" })
})
} catch (e) {
console.log("rollback:", await AppDataSource.getRepository(User).count())
}
このコードを実行すると、UNIQUE constraint failed: user.email で2件目の INSERT が失敗し、ROLLBACK が発行されました。1件目の [email protected] も残らず、ユーザー数は1件のままでした。コールバック内で AppDataSource.getRepository() を直接使うと、その操作がトランザクションに参加することは保証されません。公式ドキュメントの指示どおり、必ず manager を使ってください。
接続の取得や解放を自分で制御したい場合は、dataSource.createQueryRunner() で startTransaction()・commitTransaction()・rollbackTransaction() を呼び、finally で release() します。1.0からは await using 構文(TypeScript 5.2以降)で、QueryRunnerを自動で解放できます。分離レベルは、DataSourceの isolationLevel で全ドライバの既定値を設定できるようになりました。
よくあるエラーと対処
No metadata for "X" was found:エンティティがentitiesに登録されていない、またはglobの拡張子が実行環境(.tsか.jsか)と合っていません。- Undefined value encountered in property …:1.x系で、where条件にundefinedが入っています。その項目で絞り込まないなら条件オブジェクトから除き、NULLの行だけを探したいなら
IsNull()を使います。 - Empty criteria(s) are not allowed for the update method.:1.1.0以降で、
update()やdelete()の条件が空になっています。 - 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を同じ書き方で扱いたい場合です。1.0が出たことで、長く言われていた「開発が止まっている」という懸念は当てはまらなくなりました。開発者向け文書によると、nightly版は毎日UTC 2時に、前回の公開以降にコミットがあれば自動で公開されます。2026年9月6日から17日までは、毎日公開されていました。
避けるべきなのは、型安全を最優先する場合です。getRawMany() の戻り値や、QueryBuilderに文字列で書いた列名は、コンパイル時に検査されません。スキーマファイルから型を生成するPrismaや、SQLに近い書き方で結果の型まで推論するDrizzleのほうが、列名の打ち間違いをコンパイル時に見つけられます。PrismaとTypeORMの比較はTypeORMとPrismaの比較記事で、Drizzleの特徴はDrizzle ORMの解説で詳しく扱っています。
よくある質問
TypeORMとは何ですか?
TypeScriptのクラスとデコレーターでテーブルを定義し、リポジトリやQueryBuilderでDBを操作するNode.js向けのORMです。PostgreSQL、MySQL、SQLite、SQL Server、Oracle、MongoDBなどに対応しています。
TypeORMの最新バージョンはいくつですか?
2026年9月17日時点で、npmのlatestは1.1.1(2026年9月1日公開)です。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は起動時に落ちます。環境変数から接続情報を読む設定は、ConfigModuleでNestJSの環境変数を型安全に管理する方法を参考にしてください。
synchronize: trueは使ってもよいですか?
開発初期のローカル環境に限れば使えます。公式ドキュメントは本番での使用を禁じています。共有DBと本番では、migration:generate で差分を確認してから migration:run で適用します。