Migrate from TypeORM

This guide describes how to migrate from TypeORM to Prisma. It uses an extended version of the TypeORM Express example as a sample project to demonstrate the migration steps. You can find the example used for this guide on GitHub.

This migration guide uses PostgreSQL as the example database, but it equally applies to any other relational database that's supported by Prisma.

You can learn how Prisma compares to TypeORM on the Prisma vs TypeORM page.

Overview of the migration process

Note that the steps for migrating from TypeORM to Prisma are always the same, no matter what kind of application or API layer you're building:

  1. Install the Prisma CLI
  2. Introspect your database
  3. Create a baseline migration
  4. Install Prisma Client
  5. Gradually replace your TypeORM queries with Prisma Client

These steps apply, no matter if you're building a REST API (e.g. with Express, koa or NestJS), a GraphQL API (e.g. with Apollo Server, TypeGraphQL or Nexus) or any other kind of application that uses TypeORM for database access.

Prisma lends itself really well for incremental adoption. This means, you don't have migrate your entire project from TypeORM to Prisma at once, but rather you can step-by-step move your database queries from TypeORM to Prisma.

Overview of the sample project

For this guide, we'll use a REST API built with Express as a sample project to migrate to Prisma. It has four models/entities:

User.ts
Post.ts
Profile.ts
Category.ts
@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
}

The models have the following relations:

  • 1-1: UserProfile
  • 1-n: UserPost
  • m-n: PostCategory

The corresponding tables have been created using a generated TypeORM migration.

The migration has been created using

$typeorm migration:generate -n Init

This created the following migration file:

migrations/1605698662257-Init.ts
1import { MigrationInterface, QueryRunner } from 'typeorm'
2
3export class Init1605698662257 implements MigrationInterface {
4 name = 'Init1605698662257'
5
6 public async up(queryRunner: QueryRunner): Promise<void> {
7 await queryRunner.query(
8 `CREATE TABLE "profile" ("id" SERIAL NOT NULL, "bio" character varying, "userId" integer, CONSTRAINT "REL_a24972ebd73b106250713dcddd" UNIQUE ("userId"), CONSTRAINT "PK_3dd8bfc97e4a77c70971591bdcb" PRIMARY KEY ("id"))`
9 )
10 await queryRunner.query(
11 `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"))`
12 )
13 await queryRunner.query(
14 `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"))`
15 )
16 await queryRunner.query(
17 `CREATE TABLE "category" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_9c4e4a89e3674fc9f382d733f03" PRIMARY KEY ("id"))`
18 )
19 await queryRunner.query(
20 `CREATE TABLE "post_categories_category" ("postId" integer NOT NULL, "categoryId" integer NOT NULL, CONSTRAINT "PK_91306c0021c4901c1825ef097ce" PRIMARY KEY ("postId", "categoryId"))`
21 )
22 await queryRunner.query(
23 `CREATE INDEX "IDX_93b566d522b73cb8bc46f7405b" ON "post_categories_category" ("postId") `
24 )
25 await queryRunner.query(
26 `CREATE INDEX "IDX_a5e63f80ca58e7296d5864bd2d" ON "post_categories_category" ("categoryId") `
27 )
28 await queryRunner.query(
29 `ALTER TABLE "profile" ADD CONSTRAINT "FK_a24972ebd73b106250713dcddd9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
30 )
31 await queryRunner.query(
32 `ALTER TABLE "post" ADD CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
33 )
34 await queryRunner.query(
35 `ALTER TABLE "post_categories_category" ADD CONSTRAINT "FK_93b566d522b73cb8bc46f7405bd" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
36 )
37 await queryRunner.query(
38 `ALTER TABLE "post_categories_category" ADD CONSTRAINT "FK_a5e63f80ca58e7296d5864bd2d3" FOREIGN KEY ("categoryId") REFERENCES "category"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
39 )
40 }
41
42 public async down(queryRunner: QueryRunner): Promise<void> {
43 await queryRunner.query(
44 `ALTER TABLE "post_categories_category" DROP CONSTRAINT "FK_a5e63f80ca58e7296d5864bd2d3"`
45 )
46 await queryRunner.query(
47 `ALTER TABLE "post_categories_category" DROP CONSTRAINT "FK_93b566d522b73cb8bc46f7405bd"`
48 )
49 await queryRunner.query(
50 `ALTER TABLE "post" DROP CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0"`
51 )
52 await queryRunner.query(
53 `ALTER TABLE "profile" DROP CONSTRAINT "FK_a24972ebd73b106250713dcddd9"`
54 )
55 await queryRunner.query(`DROP INDEX "IDX_a5e63f80ca58e7296d5864bd2d"`)
56 await queryRunner.query(`DROP INDEX "IDX_93b566d522b73cb8bc46f7405b"`)
57 await queryRunner.query(`DROP TABLE "post_categories_category"`)
58 await queryRunner.query(`DROP TABLE "category"`)
59 await queryRunner.query(`DROP TABLE "post"`)
60 await queryRunner.query(`DROP TABLE "user"`)
61 await queryRunner.query(`DROP TABLE "profile"`)
62 }
63}

As mentioned before, this guide is an extended variation of the TypeORM Express example and uses the same file structure. The route handlers are located in the src/controller directory. From there, they are pulled into a central src/routes.ts file which is used to set up the required routes in 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

Step 1. Install the Prisma CLI

The first step to adopt Prisma is to install the Prisma CLI in your project:

$npm install prisma --save-dev

Step 2. Introspect your database

2.1. Set up Prisma

Before you can introspect your database, you need to set up your Prisma schema and connect Prisma to your database. Run the following command in your terminal to create a basic Prisma schema file:

$npx prisma init

This command created a new directory called prisma with the following files for you:

  • schema.prisma: Your Prisma schema file that specifies your database connection and models
  • .env: A dotenv to configure your database connection URL as an environment variable

The Prisma schema file currently looks as follows:

prisma/schema.prisma
1// This is your Prisma schema file,
2// learn more about it in the docs: https://pris.ly/d/prisma-schema
3
4datasource db {
5 provider = "postgresql"
6 url = env("DATABASE_URL")
7}
8
9generator client {
10 provider = "prisma-client-js"
11}

If you're using VS Code, be sure to install the Prisma VS Code extension for syntax highlighting, formatting, auto-completion and a lot more cool features.

2.2. Connect your database

If you're not using PostgreSQL, you need to adjust the provider field on the datasource block to the database you currently use:

PostgreSQL
MySQL
Microsoft SQL Server
SQLite
schema.prisma
1datasource db {
2 provider = "postgresql"
3 url = env("DATABASE_URL")
4}

Once that's done, you can configure your database connection URL in the .env file. Here's how the database connection from TypeORM maps to the connection URL format used by Prisma:

PostgreSQL
MySQL
Microsoft SQL Server
SQLite

Assume you have the following database connection details in ormconfig.json:

ormconfig.json
1{
2 "type": "postgres",
3 "host": "localhost",
4 "port": 5432,
5 "username": "alice",
6 "password": "myPassword42",
7 "database": "blog-typeorm"
8}

The respective connection URL would look as follows in Prisma:

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

Note that you can optionally configure the PostgreSQL schema by appending the schema argument to the connection URL:

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

If not provided, the default schema called public is being used.

2.3. Introspect your database using Prisma

With your connection URL in place, you can introspect your database to generate your Prisma models:

$npx prisma db pull

This creates the following Prisma models:

prisma/schema.prisma
1model typeorm_migrations {
2 id Int @id @default(autoincrement())
3 timestamp Int
4 name String
5
6 @@map("_typeorm_migrations")
7}
8
9model category {
10 id Int @id @default(autoincrement())
11 name String
12 post_categories_category post_categories_category[]
13}
14
15model post {
16 id Int @id @default(autoincrement())
17 title String
18 content String?
19 published Boolean @default(false)
20 authorId Int?
21 user user? @relation(fields: [authorId], references: [id])
22 post_categories_category post_categories_category[]
23}
24
25model post_categories_category {
26 postId Int
27 categoryId Int
28 category category @relation(fields: [categoryId], references: [id])
29 post post @relation(fields: [postId], references: [id])
30
31 @@id([postId, categoryId])
32 @@index([postId], name: "IDX_93b566d522b73cb8bc46f7405b")
33 @@index([categoryId], name: "IDX_a5e63f80ca58e7296d5864bd2d")
34}
35
36model profile {
37 id Int @id @default(autoincrement())
38 bio String?
39 userId Int? @unique
40 user user? @relation(fields: [userId], references: [id])
41}
42
43model user {
44 id Int @id @default(autoincrement())
45 name String?
46 email String @unique
47 post post[]
48 profile profile?
49}

The generated Prisma models represent your database tables and are the foundation for your programmatic Prisma Client API which allows you to send queries to your database.

2.4. Create a baseline migration

To continue using Prisma Migrate to evolve your database schema, you will need to baseline your database.

First, create a migrations directory and add a directory inside with your preferred name for the migration. In this example, we will use 0_init as the migration name:

$mkdir -p prisma/migrations/0_init

Next, generate the migration file with prisma migrate diff. Use the following arguments:

  • --from-empty: assumes the data model you're migrating from is empty
  • --to-schema-datamodel: the current database state using the URL in the datasource block
  • --script: output a SQL script
$npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql

Review the generated migration to ensure everything is correct.

Next, mark the migration as applied using prisma migrate resolve with the --applied argument.

$npx prisma migrate resolve --applied 0_init

The command will mark 0_init as applied by adding it to the _prisma_migrations table.

You now have a baseline for your current database schema. To make further changes to your database schema, you can update your Prisma schema and use prisma migrate dev to apply the changes to your database.

2.5. Adjust the Prisma schema (optional)

The models that were generated via introspection currently exactly map to your database tables. In this section, you'll learn how you can adjust the naming of the Prisma models to adhere to Prisma's naming conventions.

All of these adjustment are entirely optional and you are free to skip to the next step already if you don't want to adjust anything for now. You can go back and make the adjustments at any later point.

As opposed to the current snake_case notation of TypeORM models, Prisma's naming conventions are:

  • PascalCase for model names
  • camelCase for field names

You can adjust the naming by mapping the Prisma model and field names to the existing table and column names in the underlying database using @@map and @map.

Also note that you can rename relation fields to optimize the Prisma Client API that you'll use later to send queries to your database. For example, the post field on the user model is a list, so a better name for this field would be posts to indicate that it's plural.

You can further completely remove model that represents the TypeORM migrations table (called _typeorm_migrations here) from the Prisma schema.

Here's an adjusted version of the Prisma schema that addresses these points:

prisma/schema.prisma
1model Category {
2 id Int @id @default(autoincrement())
3 name String
4 postsToCategories PostToCategories[]
5
6 @@map("category")
7}
8
9model Post {
10 id Int @id @default(autoincrement())
11 title String
12 content String?
13 published Boolean @default(false)
14 authorId Int?
15 author User? @relation(fields: [authorId], references: [id])
16 postsToCategories PostToCategories[]
17
18 @@map("post")
19}
20
21model PostToCategories {
22 postId Int
23 categoryId Int
24 category Category @relation(fields: [categoryId], references: [id])
25 post Post @relation(fields: [postId], references: [id])
26
27
28 @@id([postId, categoryId])
29 @@index([postId], name: "IDX_93b566d522b73cb8bc46f7405b")
30 @@index([categoryId], name: "IDX_a5e63f80ca58e7296d5864bd2d")
31 @@map("post_categories_category")
32}
33
34model Profile {
35 id Int @id @default(autoincrement())
36 bio String?
37 userId Int? @unique
38 user User? @relation(fields: [userId], references: [id])
39
40 @@map("profile")
41}
42
43model User {
44 id Int @id @default(autoincrement())
45 name String?
46 email String @unique
47 posts Post[]
48 profile Profile?
49
50 @@map("user")
51}

Step 3. Install Prisma Client

As a next step, you can install Prisma Client in your project so that you can start replacing the database queries in your project that are currently made with TypeORM:

$npm install @prisma/client

Step 4. Replace your TypeORM queries with Prisma Client

In this section, we'll show a few sample queries that are being migrated from TypeORM to Prisma Client based on the example routes from the sample REST API project. For a comprehensive overview of how the Prisma Client API differs from TypeORM, check out the API comparison page.

First, to set up the PrismaClient instance that you'll use to send database queries from the various route handlers. Create a new file named prisma.ts in the src directory:

$touch src/prisma.ts

Now, instantiate PrismaClient and export it from the file so you can use it in your route handlers later:

src/prisma.ts
1import { PrismaClient } from '@prisma/client'
2
3export const prisma = new PrismaClient()

4.1. Replacing queries in GET requests

The REST API has three routes that accept GET requests:

  • /feed: Return all published posts
  • /filterPosts?searchString=SEARCH_STRING: Filter returned posts by SEARCH_STRING
  • /post/:postId: Returns a specific post

Let's dive into the route handlers that implement these requests.

/feed

The /feed handler is currently implemented as follows:

src/controllers/FeedAction.ts
1import { getManager } from 'typeorm'
2import { Post } from '../entity/Post'
3
4export async function feedAction(req, res) {
5 const postRepository = getManager().getRepository(Post)
6
7 const publishedPosts = await postRepository.find({
8 where: { published: true },
9 relations: ['author'],
10 })
11
12 res.send(publishedPosts)
13}

Note that each returned Post object includes the relation to the author it's associated with. With TypeORM, including the relation is not type-safe. For example, if there was a typo in the relation that is retrieved, your database query would fail only at runtime – the TypeScript compiler does not provide any safety here.

Here is how the same route is implemented using Prisma Client:

src/controllers/FeedAction.ts
1import { prisma } from '../prisma'
2
3export async function feedAction(req, res) {
4 const publishedPosts = await prisma.post.findMany({
5 where: { published: true },
6 include: { author: true },
7 })
8
9 res.send(publishedPosts)
10}

Note that the way how Prisma Client includes the author relation is absolutely type-safe. The TypeScript compiler would throw an error if you were trying to include a relation that does not exist on the Post model.

/filterPosts?searchString=SEARCH_STRING

The /filterPosts handler is currently implemented as follows:

src/controllers/FilterPostsActions.ts
1import { getManager, Like } from 'typeorm'
2import { Post } from '../entity/Post'
3
4export async function filterPostsAction(req, res) {
5 const { searchString } = req.query
6 const postRepository = getManager().getRepository(Post)
7
8 const filteredPosts = await postRepository.find({
9 where: [
10 { title: Like(`%${searchString}%`) },
11 { content: Like(`%${searchString}%`) },
12 ],
13 })
14
15 res.send(filteredPosts)
16}

With Prisma, the route is implemented as follows:

src/controllers/FilterPostsActions.ts
1import { prisma } from '../prisma'
2
3export async function filterPostsAction(req, res) {
4 const { searchString } = req.query
5
6 const filteredPosts = prisma.post.findMany({
7 where: {
8 OR: [
9 {
10 title: { contains: searchString },
11 },
12 {
13 content: { contains: searchString },
14 },
15 ],
16 },
17 })
18
19 res.send(filteredPosts)
20}

Note that TypeORM by default combines several where conditions with an implicit OR operator. Prisma on the other hand combines several where conditions with an implicit AND operator, so in this case the Prisma query needs to make the OR explicit.

/post/:postId

The /post/:postId handler is currently implemented as follows:

src/controllers/GetPostByIdAction.ts
1import { getManager } from 'typeorm'
2import { Post } from '../entity/Post'
3
4export async function getPostByIdAction(req, res) {
5 const { postId } = req.params
6 const postRepository = getManager().getRepository(Post)
7
8 const post = await postRepository.findOne(postId)
9
10 res.send(post)
11}

With Prisma, the route is implemented as follows:

src/controllers/GetPostByIdAction.ts
1import { prisma } from '../prisma'
2
3export async function getPostByIdAction(req, res) {
4 const { postId } = req.params
5
6 const post = await prisma.post.findUnique({
7 where: { id: postId },
8 })
9
10 res.send(post)
11}

4.2. Replacing queries in POST requests

The REST API has three routes that accept POST requests:

  • /user: Creates a new User record
  • /post: Creates a new Post record
  • /user/:userId/profile: Creates a new Profile record for a User record with a given ID

/user

The /user handler is currently implemented as follows:

src/controllers/CreateUserAction.ts
1import { getManager } from 'typeorm'
2import { User } from '../entity/User'
3
4export async function createUserAction(req, res) {
5 const { name, email } = req.body
6
7 const userRepository = getManager().getRepository(User)
8
9 const newUser = new User()
10 newUser.name = name
11 newUser.email = email
12 userRepository.save(newUser)
13
14 res.send(newUser)
15}

With Prisma, the route is implemented as follows:

src/controllers/CreateUserAction.ts
1import { prisma } from '../prisma'
2
3export async function createUserAction(req, res) {
4 const { name, email } = req.body
5
6 const newUser = await prisma.user.create({
7 data: {
8 name,
9 email,
10 },
11 })
12
13 res.send(newUser)
14}

/post

The /post handler is currently implemented as follows:

src/controllers/CreateDraftAction.ts
1import { getManager } from 'typeorm'
2import { Post } from '../entity/Post'
3import { User } from '../entity/User'
4
5export async function createDraftAction(req, res) {
6 const { title, content, authorEmail } = req.body
7
8 const userRepository = getManager().getRepository(User)
9 const user = await userRepository.findOne({ email: authorEmail })
10
11 const postRepository = getManager().getRepository(Post)
12
13 const newPost = new Post()
14 newPost.title = title
15 newPost.content = content
16 newPost.author = user
17 postRepository.save(newPost)
18
19 res.send(newPost)
20}

With Prisma, the route is implemented as follows:

src/controllers/CreateDraftAction.ts
1import { prisma } from '../prisma'
2
3export async function createDraftAction(req, res) {
4 const { title, content, authorEmail } = req.body
5
6 const newPost = await prisma.post.create({
7 data: {
8 title,
9 content,
10 author: {
11 connect: { email: authorEmail },
12 },
13 },
14 })
15
16 res.send(newPost)
17}

Note that Prisma Client's nested write here save an initial query where first the User record needs to be retrieved by its email. That's because, with Prisma you can connect records in relations using any unique property.

/user/:userId/profile

The /user/:userId/profile handler is currently implemented as follows:

src/controllers/SetBioForUserAction.ts.ts
1import { getManager } from 'typeorm'
2import { Profile } from '../entity/Profile'
3import { User } from '../entity/User'
4
5export async function setBioForUserAction(req, res) {
6 const { userId } = req.params
7 const { bio } = req.body
8
9 const userRepository = getManager().getRepository(User)
10 const user = await userRepository.findOne(userId, {
11 relations: ['profile'],
12 })
13
14 const profileRepository = getManager().getRepository(Profile)
15 user.profile.bio = bio
16
17 profileRepository.save(user.profile)
18
19 res.send(user)
20}

With Prisma, the route is implemented as follows:

src/controllers/SetBioForUserAction.ts.ts
1import { prisma } from '../prisma'
2
3export async function setBioForUserAction(req, res) {
4 const { userId } = req.params
5 const { bio } = req.body
6
7 const user = await prisma.user.update({
8 where: { id: userId },
9 data: {
10 profile: {
11 update: {
12 bio,
13 },
14 },
15 },
16 })
17
18 res.send(user)
19}

4.3. Replacing queries in PUT requests

The REST API has one route that accept a PUT request:

  • /addPostToCategory?postId=POST_ID&categoryId=CATEGORY_ID: Adds the post with POST_ID to the category with CATEGORY_ID

Let's dive into the route handlers that implement these requests.

/addPostToCategory?postId=POST_ID&categoryId=CATEGORY_ID

The /addPostToCategory?postId=POST_ID&categoryId=CATEGORY_ID handler is currently implemented as follows:

src/controllers/AddPostToCategoryAction.ts
1import { getManager } from 'typeorm'
2import { Post } from '../entity/Post'
3import { Category } from '../entity/Category'
4
5export async function addPostToCategoryAction(req, res) {
6 const { postId, categoryId } = req.query
7
8 const postRepository = getManager().getRepository(Post)
9 const post = await postRepository.findOne(postId, {
10 relations: ['categories'],
11 })
12
13 const categoryRepository = getManager().getRepository(Category)
14 const category = await categoryRepository.findOne(categoryId)
15
16 post.categories.push(category)
17 postRepository.save(post)
18
19 res.send(post)
20}

With Prisma, the route is implemented as follows:

src/controllers/AddPostToCategoryAction.ts
1import { prisma } from '../prisma'
2
3export async function addPostToCategoryAction(req, res) {
4 const { postId, categoryId } = req.query
5
6 const post = await prisma.post.update({
7 data: {
8 postsToCategories: {
9 create: {
10 category: {
11 connect: { id: categoryId },
12 },
13 },
14 },
15 },
16 where: {
17 id: postId,
18 },
19 })
20
21 res.send(post)
22}

Note that this Prisma Client can be made less verbose by modeling the relation as an implicit many-to-many relation instead. In that case, the query would look as follows:

src/controllers/AddPostToCategoryAction.ts
1const post = await prisma.post.update({
2 data: {
3 categories: {
4 connect: { id: categoryId },
5 },
6 },
7 where: { id: postId },
8})

More

Implicit many-to-many relations

Similar to the @manyToMany decorator in TypeORM, Prisma allows you to model many-to-many relations implicitly. That is, a many-to-many relation where you do not have to manage the relation table (also sometimes called JOIN table) explicitly in your schema. Here is an example with 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[]
}

If you generate and run a migration with TypeORM based on these models, TypeORM will automatically create the following relation table for you:

-- 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);

If you introspect the database with Prisma, you'll get the following result in the Prisma schema (note that some relation field names have been adjusted to look friendlier compared to the raw version from introspection):

schema.prisma
1model Category {
2 id Int @id @default(autoincrement())
3 name String
4 postsToCategories PostToCategories[]
5
6 @@map("category")
7}
8
9model Post {
10 id Int @id @default(autoincrement())
11 title String
12 content String?
13 published Boolean @default(false)
14 authorId Int?
15 author User? @relation(fields: [authorId], references: [id])
16 postsToCategories PostToCategories[]
17
18 @@map("post")
19}
20
21model PostToCategories {
22 postId Int
23 categoryId Int
24 category Category @relation(fields: [categoryId], references: [id])
25 post Post @relation(fields: [postId], references: [id])
26
27 @@id([postId, categoryId])
28 @@index([postId], name: "IDX_93b566d522b73cb8bc46f7405b")
29 @@index([categoryId], name: "IDX_a5e63f80ca58e7296d5864bd2d")
30 @@map("post_categories_category")
31}

In this Prisma schema, the many-to-many relation is modeled explicitly via the relation table PostToCategories.

By adhering to the conventions for Prisma relation tables, the relation could look as follows:

schema.prisma
1model Category {
2 id Int @id @default(autoincrement())
3 name String
4 posts Post[]
5
6 @@map("category")
7}
8
9model Post {
10 id Int @id @default(autoincrement())
11 title String
12 content String?
13 published Boolean @default(false)
14 authorId Int?
15 author User? @relation(fields: [authorId], references: [id])
16 categories Category[]
17
18 @@map("post")
19}

This would also result in a more ergonomic and less verbose Prisma Client API to modify the records in this relation, because you have a direct path from Post to Category (and the other way around) instead of needing to traverse the PostToCategories model first.

If your database provider requires tables to have primary keys then you have to use explicit syntax, and manually create the join model with a primary key. This is because relation tables (JOIN tables) created by Prisma (expressed via `@relation`) for many-to-many relations using implicit syntax do not have primary keys.
Edit this page on GitHub