メインコンテンツにスキップ

TypeORMからの移行

このガイドでは、TypeORMからPrisma ORMへの移行方法について説明します。TypeORM Expressの例の拡張バージョンをサンプルプロジェクトとして使用し、移行手順を示します。このガイドで使用されている例は、GitHubにあります。

この移行ガイドでは、PostgreSQLをサンプルデータベースとして使用していますが、Prisma ORMでサポートされている他のリレーショナルデータベースにも同様に適用されます。

Prisma ORMとTypeORMの比較については、Prisma ORMとTypeORMの比較のページをご覧ください。

移行プロセスの概要

TypeORMからPrisma ORMに移行する手順は、どのようなアプリケーションやAPIレイヤーを構築しているかに関係なく、常に同じです。

  1. Prisma CLIをインストールする
  2. データベースをイントロスペクションする
  3. ベースラインマイグレーションを作成する
  4. Prisma Clientをインストールする
  5. TypeORMクエリをPrisma Clientに徐々に置き換える

これらの手順は、REST API(例:Express、koa、NestJS)、GraphQL API(例:Apollo Server、TypeGraphQL、Nexus)、またはデータベースアクセスにTypeORMを使用するその他のアプリケーションを構築しているかに関係なく適用されます。

Prisma ORMは、**段階的な導入**に非常に適しています。つまり、プロジェクト全体をTypeORMからPrisma ORMに一度に移行する必要はなく、データベースクエリをTypeORMからPrisma ORMに*段階的に*移行できます。

サンプルプロジェクトの概要

このガイドでは、Expressで構築されたREST APIをサンプルプロジェクトとして使用し、Prisma ORMに移行します。4つのモデル/エンティティがあります。

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

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

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

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

@OneToOne((type) => Profile, (profile) => profile.user, { cascade: true })
profile: Profile
}

モデルには、次のリレーションがあります。

  • 1対1:UserProfile
  • 1対多:UserPost
  • 多対多:PostCategory

対応するテーブルは、生成されたTypeORMマイグレーションを使用して作成されています。

詳細を表示するには展開してください

マイグレーションは、次を使用して作成されました。

typeorm migration:generate -n Init

これにより、次のマイグレーションファイルが作成されました。

migrations/1605698662257-Init.ts
import { MigrationInterface, QueryRunner } from 'typeorm'

export class Init1605698662257 implements MigrationInterface {
name = 'Init1605698662257'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "profile" ("id" SERIAL NOT NULL, "bio" character varying, "userId" integer, CONSTRAINT "REL_a24972ebd73b106250713dcddd" UNIQUE ("userId"), CONSTRAINT "PK_3dd8bfc97e4a77c70971591bdcb" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE TABLE "user" ("id" SERIAL NOT NULL, "name" character varying, "email" character varying NOT NULL, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE TABLE "post" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "content" character varying, "published" boolean NOT NULL DEFAULT false, "authorId" integer, CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE TABLE "category" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_9c4e4a89e3674fc9f382d733f03" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE TABLE "post_categories_category" ("postId" integer NOT NULL, "categoryId" integer NOT NULL, CONSTRAINT "PK_91306c0021c4901c1825ef097ce" PRIMARY KEY ("postId", "categoryId"))`
)
await queryRunner.query(
`CREATE INDEX "IDX_93b566d522b73cb8bc46f7405b" ON "post_categories_category" ("postId") `
)
await queryRunner.query(
`CREATE INDEX "IDX_a5e63f80ca58e7296d5864bd2d" ON "post_categories_category" ("categoryId") `
)
await queryRunner.query(
`ALTER TABLE "profile" ADD CONSTRAINT "FK_a24972ebd73b106250713dcddd9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "post" ADD CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "post_categories_category" ADD CONSTRAINT "FK_93b566d522b73cb8bc46f7405bd" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "post_categories_category" ADD CONSTRAINT "FK_a5e63f80ca58e7296d5864bd2d3" FOREIGN KEY ("categoryId") REFERENCES "category"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "post_categories_category" DROP CONSTRAINT "FK_a5e63f80ca58e7296d5864bd2d3"`
)
await queryRunner.query(
`ALTER TABLE "post_categories_category" DROP CONSTRAINT "FK_93b566d522b73cb8bc46f7405bd"`
)
await queryRunner.query(
`ALTER TABLE "post" DROP CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0"`
)
await queryRunner.query(
`ALTER TABLE "profile" DROP CONSTRAINT "FK_a24972ebd73b106250713dcddd9"`
)
await queryRunner.query(`DROP INDEX "IDX_a5e63f80ca58e7296d5864bd2d"`)
await queryRunner.query(`DROP INDEX "IDX_93b566d522b73cb8bc46f7405b"`)
await queryRunner.query(`DROP TABLE "post_categories_category"`)
await queryRunner.query(`DROP TABLE "category"`)
await queryRunner.query(`DROP TABLE "post"`)
await queryRunner.query(`DROP TABLE "user"`)
await queryRunner.query(`DROP TABLE "profile"`)
}
}

前述のように、このガイドはTypeORM Expressの例の拡張バージョンであり、同じファイル構造を使用しています。ルートハンドラーは、src/controllerディレクトリにあります。そこから、それらは中央のsrc/routes.tsファイルにプルされ、src/index.tsで必要なルートを設定するために使用されます。

└── blog-typeorm
├── ormconfig.json
├── package.json
├── src
│   ├── controllers
│   │   ├── AddPostToCategoryAction.ts
│   │   ├── CreateDraftAction.ts
│   │   ├── CreateUserAction.ts
│   │   ├── FeedAction.ts
│   │   ├── FilterPostsAction.ts
│   │   ├── GetPostByIdAction.ts
│   │   └── SetBioForUserAction.ts
│   ├── entity
│   │   ├── Category.ts
│   │   ├── Post.ts
│   │   ├── Profile.ts
│   │   └── User.ts
│   ├── index.ts
│   ├── migration
│   │   └── 1605698662257-Init.ts
│   └── routes.ts
└── tsconfig.json

ステップ1. Prisma CLIをインストールする

Prisma ORMを導入する最初のステップは、プロジェクトにPrisma CLIをインストールすることです。

npm install prisma --save-dev

ステップ2. データベースをイントロスペクションする

2.1. Prisma ORMをセットアップする

データベースをイントロスペクションする前に、Prismaスキーマをセットアップし、Prismaをデータベースに接続する必要があります。ターミナルで次のコマンドを実行して、基本的なPrismaスキーマファイルを作成します。

npx prisma init

このコマンドは、次のファイルを含むprismaという新しいディレクトリを作成しました。

  • schema.prisma:データベース接続とモデルを指定するPrismaスキーマ
  • .env:データベース接続URLを環境変数として構成するためのdotenv

Prismaスキーマは現在、次のようになっています。

prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}
ヒント

VS Codeを使用している場合は、構文の強調表示、フォーマット、自動補完、その他多くの便利な機能を提供するPrisma VS Code拡張機能を必ずインストールしてください。

2.2. データベースを接続する

PostgreSQLを使用していない場合は、`datasource`ブロックの`provider`フィールドを現在使用しているデータベースに調整する必要があります。


schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

完了したら、データベース接続URLを`.env`ファイルで構成できます。TypeORMのデータベース接続が、Prisma ORMで使用される接続URL形式にどのようにマッピングされるかを以下に示します。


`ormconfig.json`に次のデータベース接続の詳細があるとします。


ormconfig.json
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "alice",
"password": "myPassword42",
"database": "blog-typeorm"
}

それぞれの接続URLは、Prisma ORMでは次のようになります。


.env
DATABASE_URL="postgresql://alice:myPassword42@localhost:5432/blog-typeorm"

`schema`引数を接続URLに追加することで、PostgreSQLのスキーマをオプションで構成できることに注意してください。


.env
DATABASE_URL="postgresql://alice:myPassword42@localhost:5432/blog-typeorm?schema=myschema"

指定しない場合、`public`と呼ばれるデフォルトスキーマが使用されます。


2.3. Prisma ORMを使用してデータベースをイントロスペクトする

接続URLを設定したら、イントロスペクト機能を使用してデータベースからPrismaモデルを生成できます。

npx prisma db pull

これにより、以下のPrismaモデルが作成されます。

prisma/schema.prisma
model typeorm_migrations {
id Int @id @default(autoincrement())
timestamp Int
name String

@@map("_typeorm_migrations")
}

model category {
id Int @id @default(autoincrement())
name String
post_categories_category post_categories_category[]
}

model post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
user user? @relation(fields: [authorId], references: [id])
post_categories_category post_categories_category[]
}

model post_categories_category {
postId Int
categoryId Int
category category @relation(fields: [categoryId], references: [id])
post post @relation(fields: [postId], references: [id])

@@id([postId, categoryId])
@@index([postId], name: "IDX_93b566d522b73cb8bc46f7405b")
@@index([categoryId], name: "IDX_a5e63f80ca58e7296d5864bd2d")
}

model profile {
id Int @id @default(autoincrement())
bio String?
userId Int? @unique
user user? @relation(fields: [userId], references: [id])
}

model user {
id Int @id @default(autoincrement())
name String?
email String @unique
post post[]
profile profile?
}

生成されたPrismaモデルはデータベースのテーブルを表しており、データベースにクエリを送信するためのプログラム的なPrisma Client APIの基盤となります。

2.4. ベースラインマイグレーションを作成する

Prisma Migrateを使用してデータベーススキーマを進化させ続けるには、データベースのベースライン化を行う必要があります。

まず、migrationsディレクトリを作成し、その中にマイグレーションの名前を付けたディレクトリを追加します。この例では、マイグレーション名として0_initを使用します。

mkdir -p prisma/migrations/0_init

次に、prisma migrate diffコマンドでマイグレーションファイルを生成します。以下の引数を使用します。

  • --from-empty: マイグレーション元のデータモデルが空であると仮定します。
  • --to-schema-datamodel: datasourceブロックのURLを使用して、現在のデータベースの状態を指定します。
  • --script: SQLスクリプトを出力します。
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql

生成されたマイグレーションを確認し、すべてが正しいことを確認してください。

次に、prisma migrate resolveコマンドに--applied引数を指定して、マイグレーションが適用済みとしてマークします。

npx prisma migrate resolve --applied 0_init

このコマンドは、0_init_prisma_migrationsテーブルに追加することで、適用済みとしてマークします。

これで、現在のデータベーススキーマのベースラインが作成されました。データベーススキーマをさらに変更するには、Prismaスキーマを更新し、prisma migrate devコマンドを使用して変更をデータベースに適用します。

2.5. Prismaスキーマを調整する(オプション)

イントロスペクションによって生成されたモデルは、現在、データベーステーブルと*完全に*一致しています。このセクションでは、Prisma ORMの命名規則に準拠するようにPrismaモデルの名前を調整する方法を学びます。

これらの調整はすべてオプションであり、今のところ何も調整したくない場合は、次のステップに進んでください。後でいつでも戻って調整を行うことができます。

現在のTypeORMモデルのスネークケース記法とは対照的に、Prisma ORMの命名規則は以下のとおりです。

  • モデル名:パスカルケース
  • フィールド名:キャメルケース

@@map@mapを使用して、Prismaのモデル名とフィールド名を、基盤となるデータベースの既存のテーブル名と列名に*マッピング*することで、命名を調整できます。

また、後でデータベースにクエリを送信するために使用するPrisma Client APIを最適化するために、リレーションフィールドの名前を変更することもできます。たとえば、userモデルのpostフィールドは*リスト*であるため、複数形であることを示すために、このフィールドの名前をpostsにする方が適切です。

さらに、TypeORMのマイグレーションテーブル(ここでは_typeorm_migrationsと呼ばれています)を表すモデルをPrismaスキーマから完全に削除することもできます。

これらの点を考慮したPrismaスキーマの調整バージョンを以下に示します。

prisma/schema.prisma
model Category {
id Int @id @default(autoincrement())
name String
postsToCategories PostToCategories[]

@@map("category")
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
postsToCategories PostToCategories[]

@@map("post")
}

model PostToCategories {
postId Int
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
post Post @relation(fields: [postId], references: [id])

@@id([postId, categoryId])
@@index([postId], name: "IDX_93b566d522b73cb8bc46f7405b")
@@index([categoryId], name: "IDX_a5e63f80ca58e7296d5864bd2d")
@@map("post_categories_category")
}

model Profile {
id Int @id @default(autoincrement())
bio String?
userId Int? @unique
user User? @relation(fields: [userId], references: [id])

@@map("profile")
}

model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
profile Profile?

@@map("user")
}

ステップ3. Prisma Clientをインストールする

次のステップとして、プロジェクトにPrisma Clientをインストールして、現在TypeORMで行われているプロジェクトのデータベースクエリを置き換えられるようにします。

npm install @prisma/client

ステップ4. TypeORMクエリをPrisma Clientに置き換える

このセクションでは、サンプルREST APIプロジェクトのサンプルルートに基づいて、TypeORMからPrisma Clientに移行されるいくつかのサンプルクエリを紹介します。Prisma Client APIとTypeORMの違いの包括的な概要については、API比較ページを参照してください。

まず、さまざまなルートハンドラからデータベースクエリを送信するために使用するPrismaClientインスタンスを設定します。srcディレクトリにprisma.tsという名前の新しいファイルを作成します。

touch src/prisma.ts

次に、PrismaClientをインスタンス化し、ファイルからエクスポートして、後でルートハンドラで使用できるようにします。

src/prisma.ts
import { PrismaClient } from '@prisma/client'

export const prisma = new PrismaClient()

4.1. `GET`リクエストのクエリを置き換える

REST APIには、`GET`リクエストを受け入れる3つのルートがあります。

  • /feed: 公開されているすべての投稿を返します。
  • /filterPosts?searchString=SEARCH_STRING: 返される投稿を`SEARCH_STRING`でフィルタリングします。
  • /post/:postId: 特定の投稿を返します。

これらのリクエストを実装するルートハンドラを見てみましょう。

/feed

/feedハンドラは、現在、次のように実装されています。

src/controllers/FeedAction.ts
import { getManager } from 'typeorm'
import { Post } from '../entity/Post'

export async function feedAction(req, res) {
const postRepository = getManager().getRepository(Post)

const publishedPosts = await postRepository.find({
where: { published: true },
relations: ['author'],
})

res.send(publishedPosts)
}

返される各`Post`オブジェクトには、関連付けられている`author`へのリレーションが含まれていることに注意してください。TypeORMでは、リレーションを含めることは型安全ではありません。たとえば、取得されるリレーションにタイプミスがあった場合、データベースクエリは*実行時*にのみ失敗します。TypeScriptコンパイラは、ここでは安全性を提供しません。

Prisma Clientを使用して同じルートを実装する方法は次のとおりです。

src/controllers/FeedAction.ts
import { prisma } from '../prisma'

export async function feedAction(req, res) {
const publishedPosts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
})

res.send(publishedPosts)
}

Prisma Clientが`author`リレーションを含める方法は、完全に型安全であることに注意してください。`Post`モデルに存在しないリレーションを含めようとすると、TypeScriptコンパイラはエラーをスローします。

/filterPosts?searchString=SEARCH_STRING

/filterPosts`ハンドラは、現在、次のように実装されています。

src/controllers/FilterPostsActions.ts
import { getManager, Like } from 'typeorm'
import { Post } from '../entity/Post'

export async function filterPostsAction(req, res) {
const { searchString } = req.query
const postRepository = getManager().getRepository(Post)

const filteredPosts = await postRepository.find({
where: [
{ title: Like(`%${searchString}%`) },
{ content: Like(`%${searchString}%`) },
],
})

res.send(filteredPosts)
}

Prisma ORMでは、ルートは次のように実装されます。

src/controllers/FilterPostsActions.ts
import { prisma } from '../prisma'

export async function filterPostsAction(req, res) {
const { searchString } = req.query

const filteredPosts = prisma.post.findMany({
where: {
OR: [
{
title: { contains: searchString },
},
{
content: { contains: searchString },
},
],
},
})

res.send(filteredPosts)
}

TypeORMはデフォルトで、複数の`where`条件を暗黙の`OR`演算子で結合することに注意してください。一方、Prisma ORMは複数の`where`条件を暗黙の`AND`演算子で結合します。そのため、この場合はPrisma Clientクエリで`OR`を明示的に指定する必要があります。

/post/:postId

/post/:postIdハンドラは、現在、次のように実装されています。

src/controllers/GetPostByIdAction.ts
import { getManager } from 'typeorm'
import { Post } from '../entity/Post'

export async function getPostByIdAction(req, res) {
const { postId } = req.params
const postRepository = getManager().getRepository(Post)

const post = await postRepository.findOne(postId)

res.send(post)
}

Prisma ORMでは、ルートは次のように実装されます。

src/controllers/GetPostByIdAction.ts
import { prisma } from '../prisma'

export async function getPostByIdAction(req, res) {
const { postId } = req.params

const post = await prisma.post.findUnique({
where: { id: postId },
})

res.send(post)
}

4.2. `POST`リクエストのクエリを置き換える

REST APIには、`POST`リクエストを受け入れる3つのルートがあります。

  • /user: 新しい`User`レコードを作成します。
  • /post: 新しい`Post`レコードを作成します。
  • /user/:userId/profile: 指定されたIDの`User`レコードに新しい`Profile`レコードを作成します。

/user

/userハンドラは、現在、次のように実装されています。

src/controllers/CreateUserAction.ts
import { getManager } from 'typeorm'
import { User } from '../entity/User'

export async function createUserAction(req, res) {
const { name, email } = req.body

const userRepository = getManager().getRepository(User)

const newUser = new User()
newUser.name = name
newUser.email = email
userRepository.save(newUser)

res.send(newUser)
}

Prisma ORMでは、ルートは次のように実装されます。

src/controllers/CreateUserAction.ts
import { prisma } from '../prisma'

export async function createUserAction(req, res) {
const { name, email } = req.body

const newUser = await prisma.user.create({
data: {
name,
email,
},
})

res.send(newUser)
}

/post

/postハンドラは、現在、次のように実装されています。

src/controllers/CreateDraftAction.ts
import { getManager } from 'typeorm'
import { Post } from '../entity/Post'
import { User } from '../entity/User'

export async function createDraftAction(req, res) {
const { title, content, authorEmail } = req.body

const userRepository = getManager().getRepository(User)
const user = await userRepository.findOne({ email: authorEmail })

const postRepository = getManager().getRepository(Post)

const newPost = new Post()
newPost.title = title
newPost.content = content
newPost.author = user
postRepository.save(newPost)

res.send(newPost)
}

Prisma ORMでは、ルートは次のように実装されます。

src/controllers/CreateDraftAction.ts
import { prisma } from '../prisma'

export async function createDraftAction(req, res) {
const { title, content, authorEmail } = req.body

const newPost = await prisma.post.create({
data: {
title,
content,
author: {
connect: { email: authorEmail },
},
},
})

res.send(newPost)
}

Prisma Clientのネストされた書き込みでは、最初に`User`レコードをその`email`で取得する必要がある初期クエリが不要になることに注意してください。これは、Prisma Clientでは、任意の一意のプロパティを使用して、リレーション内のレコードを接続できるためです。

/user/:userId/profile

/user/:userId/profileハンドラは、現在、次のように実装されています。

src/controllers/SetBioForUserAction.ts.ts
import { getManager } from 'typeorm'
import { Profile } from '../entity/Profile'
import { User } from '../entity/User'

export async function setBioForUserAction(req, res) {
const { userId } = req.params
const { bio } = req.body

const userRepository = getManager().getRepository(User)
const user = await userRepository.findOne(userId, {
relations: ['profile'],
})

const profileRepository = getManager().getRepository(Profile)
user.profile.bio = bio

profileRepository.save(user.profile)

res.send(user)
}

Prisma ORMでは、ルートは次のように実装されます。

src/controllers/SetBioForUserAction.ts.ts
import { prisma } from '../prisma'

export async function setBioForUserAction(req, res) {
const { userId } = req.params
const { bio } = req.body

const user = await prisma.user.update({
where: { id: userId },
data: {
profile: {
update: {
bio,
},
},
},
})

res.send(user)
}

4.3. `PUT`リクエストのクエリを置き換える

REST APIには、`PUT`リクエストを受け入れるルートが1つあります。

  • /addPostToCategory?postId=POST_ID&categoryId=CATEGORY_ID: `POST_ID`の投稿を`CATEGORY_ID`のカテゴリに追加します。

これらのリクエストを実装するルートハンドラを見てみましょう。

/addPostToCategory?postId=POST_ID&categoryId=CATEGORY_ID

/addPostToCategory?postId=POST_ID&categoryId=CATEGORY_IDハンドラは、現在、次のように実装されています。

src/controllers/AddPostToCategoryAction.ts
import { getManager } from 'typeorm'
import { Post } from '../entity/Post'
import { Category } from '../entity/Category'

export async function addPostToCategoryAction(req, res) {
const { postId, categoryId } = req.query

const postRepository = getManager().getRepository(Post)
const post = await postRepository.findOne(postId, {
relations: ['categories'],
})

const categoryRepository = getManager().getRepository(Category)
const category = await categoryRepository.findOne(categoryId)

post.categories.push(category)
postRepository.save(post)

res.send(post)
}

Prisma ORMでは、ルートは次のように実装されます。

src/controllers/AddPostToCategoryAction.ts
import { prisma } from '../prisma'

export async function addPostToCategoryAction(req, res) {
const { postId, categoryId } = req.query

const post = await prisma.post.update({
data: {
postsToCategories: {
create: {
category: {
connect: { id: categoryId },
},
},
},
},
where: {
id: postId,
},
})

res.send(post)
}

リレーションを暗黙の多対多リレーションとしてモデル化することで、このPrisma Clientをより簡潔にすることができることに注意してください。その場合、クエリは次のようになります。

src/controllers/AddPostToCategoryAction.ts
const post = await prisma.post.update({
data: {
categories: {
connect: { id: categoryId },
},
},
where: { id: postId },
})

詳細

暗黙の多対多リレーション

TypeORMの`@manyToMany`デコレータと同様に、Prisma ORMでは、多対多リレーションを*暗黙的に*モデル化できます。つまり、スキーマでリレーションテーブル(結合テーブルとも呼ばれます)を*明示的に*管理する必要がない多対多リレーションです。TypeORMの例を以下に示します。

import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
JoinTable,
} from 'typeorm'
import { Category } from './Category'

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

@ManyToMany((type) => Category, (category) => category.posts)
@JoinTable()
categories: Category[]
}
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm'
import { Post } from './Post'

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

@ManyToMany((type) => Post, (post) => post.categories)
posts: Post[]
}

これらのモデルに基づいてTypeORMでマイグレーションを生成して実行すると、TypeORMは自動的に以下のリレーションテーブルを作成します。

-- Table Definition ----------------------------------------------
CREATE TABLE post_categories_category (
"postId" integer REFERENCES post(id) ON DELETE CASCADE,
"categoryId" integer REFERENCES category(id) ON DELETE CASCADE,
CONSTRAINT "PK_91306c0021c4901c1825ef097ce" PRIMARY KEY ("postId", "categoryId")
);

-- Indices -------------------------------------------------------
CREATE UNIQUE INDEX "PK_91306c0021c4901c1825ef097ce" ON post_categories_category("postId" int4_ops,"categoryId" int4_ops);
CREATE INDEX "IDX_93b566d522b73cb8bc46f7405b" ON post_categories_category("postId" int4_ops);
CREATE INDEX "IDX_a5e63f80ca58e7296d5864bd2d" ON post_categories_category("categoryId" int4_ops);

Prisma ORMでデータベースをイントロスペクトすると、Prismaスキーマで次の結果が得られます(イントロスペクションからの生のバージョンと比較して、一部のリレーションフィールド名がより分かりやすくなるように調整されていることに注意してください)。

schema.prisma
model Category {
id Int @id @default(autoincrement())
name String
postsToCategories PostToCategories[]

@@map("category")
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
postsToCategories PostToCategories[]

@@map("post")
}

model PostToCategories {
postId Int
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
post Post @relation(fields: [postId], references: [id])

@@id([postId, categoryId])
@@index([postId], name: "IDX_93b566d522b73cb8bc46f7405b")
@@index([categoryId], name: "IDX_a5e63f80ca58e7296d5864bd2d")
@@map("post_categories_category")
}

このPrismaスキーマでは、多対多リレーションはリレーションテーブル`PostToCategories`を介して*明示的に*モデル化されています。

Prisma ORMリレーションテーブルの規則に従うことで、リレーションは次のようになります。

schema.prisma
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]

@@map("category")
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
categories Category[]

@@map("post")
}

これにより、`PostToCategories`モデルを最初にトラバースする必要なく、`Post`から`Category`への直接パス(およびその逆)があるため、このリレーションのレコードを変更するためのより人間工学的で簡潔なPrisma Client APIが実現します。

警告

データベースプロバイダーがテーブルにプライマリキーを必須とする場合、明示的な構文を使用し、プライマリキーを持つ結合モデルを手動で作成する必要があります。これは、Prisma ORM によって作成された(`@relation` を介して表現された)多対多関係のリレーションテーブル(結合テーブル)が、暗黙的な構文を使用する場合、プライマリキーを持たないためです。