迁移自 TypeORM
本指南介绍如何从 TypeORM 迁移到 Prisma ORM。它使用 TypeORM Express 示例 的扩展版本作为 示例项目 来演示迁移步骤。您可以在 GitHub 上找到此指南中使用的示例。
此迁移指南使用 PostgreSQL 作为示例数据库,但它同样适用于 Prisma ORM 支持 的任何其他关系数据库。
您可以在 Prisma ORM 与 TypeORM 页面上了解 Prisma ORM 如何与 TypeORM 进行比较。
迁移过程概述
请注意,无论您构建的是哪种类型的应用程序或 API 层,从 TypeORM 迁移到 Prisma ORM 的步骤始终相同。
- 安装 Prisma CLI
- 内省您的数据库
- 创建基线迁移
- 安装 Prisma Client
- 逐步用 Prisma Client 替换您的 TypeORM 查询
无论您是构建 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。它有四个模型/实体
- 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
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
@ManyToMany((type) => Category, (category) => category.posts)
@JoinTable()
categories: Category[]
}
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: true })
bio: string
@OneToOne((type) => User, (user) => user.profile)
@JoinColumn()
user: User
}
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@ManyToMany((type) => Post, (post) => post.categories)
posts: Post[]
}
这些模型具有以下关系
- 1-1:
User
↔Profile
- 1-n:
User
↔Post
- m-n:
Post
↔Category
相应的表是使用生成的 TypeORM 迁移创建的。
展开以查看迁移的详细信息
迁移是使用以下方式创建的
typeorm migration:generate -n Init
这创建了以下迁移文件
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
:一个dotenv
用于将您的数据库连接 URL 配置为环境变量。
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
字段调整为您当前使用的数据库。
- PostgreSQL
- MySQL
- Microsoft SQL Server
- SQLite
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
datasource db {
provider = "sqlserver"
url = env("DATABASE_URL")
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
完成后,您可以在 .env
文件中配置您的 数据库连接 URL。以下是 TypeORM 中的数据库连接如何映射到 Prisma ORM 使用的连接 URL 格式
- PostgreSQL
- MySQL
- Microsoft SQL Server
- SQLite
假设您在 ormconfig.json
中具有以下数据库连接详细信息
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "alice",
"password": "myPassword42",
"database": "blog-typeorm"
}
相应的连接 URL 在 Prisma ORM 中如下所示
DATABASE_URL="postgresql://alice:myPassword42@localhost:5432/blog-typeorm"
请注意,您可以选择通过将 schema
参数附加到连接 URL 来配置 PostgreSQL 模式。
DATABASE_URL="postgresql://alice:myPassword42@localhost:5432/blog-typeorm?schema=myschema"
如果未提供,则使用名为 public
的默认模式。
假设您在 ormconfig.json
中具有以下数据库连接详细信息
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "alice",
"password": "myPassword42",
"database": "blog-typeorm"
}
相应的连接 URL 在 Prisma 中如下所示
DATABASE_URL="mysql://alice:myPassword42@localhost:3306/blog-typeorm"
假设您在 ormconfig.json
中具有以下数据库连接详细信息
{
"type": "mssql",
"host": "localhost",
"port": 1433,
"username": "alice",
"password": "myPassword42",
"database": "blog-typeorm"
}
相应的连接 URL 在 Prisma 中如下所示
DATABASE_URL="sqlserver://127.0.0.1:1433;database=blog-typeorm;user=alice;password=myPassword42;trustServerCertificate=true"
假设您在 ormconfig.json
中具有以下数据库连接详细信息
{
"type": "sqlite",
"database": "blog-typeorm"
}
相应的连接 URL 在 Prisma 中如下所示
DATABASE_URL="file:./blog-typeorm.db"
2.3. 使用 Prisma ORM 自省数据库
有了连接 URL 后,您可以 自省 数据库以生成 Prisma 模型。
npx prisma db pull
这将创建以下 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
该命令将通过将其添加到 _prisma_migrations
表中来标记 0_init
为已应用。
现在,您有了当前数据库模式的基线。要对数据库模式进行进一步更改,您可以更新 Prisma 模式,并使用 prisma migrate dev
将更改应用到您的数据库。
2.5. 调整 Prisma 模式(可选)
通过自省生成的模型目前完全映射到您的数据库表。在本节中,您将学习如何调整 Prisma 模型的命名以符合 Prisma ORM 的命名约定。
所有这些调整都是完全可选的,如果您现在不想进行任何调整,可以随时跳到下一步。您可以在以后的任何时间返回并进行调整。
与 TypeORM 模型当前的 snake_case 表示法相反,Prisma ORM 的命名约定是:
- 模型名称使用 PascalCase。
- 字段名称使用 camelCase。
您可以通过映射 Prisma 模型和字段名称到基础数据库中现有的表和列名称来调整命名,方法是使用 @@map
和 @map
。
另请注意,您可以重命名 关系字段 以优化您稍后将用于向数据库发送查询的 Prisma Client API。例如,user
模型上的 post
字段是一个列表,因此此字段的更好名称是 posts
以指示它是复数形式。
您可以进一步完全删除代表 TypeORM 迁移表的模型(此处称为 _typeorm_migrations
)从 Prisma 模式中。
这是一个调整后的 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. 使用 Prisma Client 替换您的 TypeORM 查询
在本节中,我们将展示一些从 TypeORM 迁移到 Prisma Client 的示例查询,这些查询基于示例 REST API 项目中的示例路由。有关 Prisma Client API 与 TypeORM 的差异的全面概述,请查看 API 比较 页面。
首先,设置您将用于从各种路由处理程序发送数据库查询的 PrismaClient
实例。在 src
目录中创建一个名为 prisma.ts
的新文件。
touch src/prisma.ts
现在,实例化 PrismaClient
并将其从文件中导出,以便您稍后可以在路由处理程序中使用它。
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()
4.1. 替换 GET
请求中的查询
REST API 有三个接受 GET
请求的路由:
/feed
:返回所有已发布的帖子。/filterPosts?searchString=SEARCH_STRING
:按SEARCH_STRING
筛选返回的帖子。/post/:postId
:返回特定帖子。
让我们深入研究实现这些请求的路由处理程序。
/feed
/feed
处理程序当前实现如下:
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 实现相同路由的方式:
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
处理程序当前实现如下:
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,该路由实现如下:
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 默认情况下使用隐式 OR
运算符组合多个 where
条件。另一方面,Prisma ORM 使用隐式 AND
运算符组合多个 where
条件,因此在本例中,Prisma Client 查询需要明确 OR
。
/post/:postId
/post/:postId
处理程序当前实现如下:
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,该路由实现如下:
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
请求的路由:
/user
:创建一个新的User
记录。/post
:创建一个新的Post
记录。/user/:userId/profile
:为具有给定 ID 的User
记录创建一个新的Profile
记录。
/user
/user
处理程序当前实现如下:
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,该路由实现如下:
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
处理程序当前实现如下:
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,该路由实现如下:
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 的嵌套写入此处保存了初始查询,其中首先需要通过其 email
检索 User
记录。这是因为,使用 Prisma Client,您可以使用任何唯一属性连接关系中的记录。
/user/:userId/profile
/user/:userId/profile
处理程序当前实现如下:
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,该路由实现如下:
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
请求的路由:
/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
处理程序当前实现如下:
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,该路由实现如下:
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 变得不那么冗长。在这种情况下,查询将如下所示:
const post = await prisma.post.update({
data: {
categories: {
connect: { id: categoryId },
},
},
where: { id: postId },
})
更多
隐式多对多关系
类似于 TypeORM 中的 @manyToMany
装饰器,Prisma ORM 允许您 隐式建模多对多关系。也就是说,在多对多关系中,您不必显式地在模式中管理 关系表(有时也称为 JOIN 表)。以下是一个 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 模式中获得以下结果(请注意,与自省的原始版本相比,一些关系字段名称已调整为更友好)。
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 关系表的约定,关系可以如下所示:
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")
}
这还将导致更符合人体工程学且更简洁的 Prisma Client API 来修改此关系中的记录,因为您有从 Post
到 Category
(反之亦然)的直接路径,而不必首先遍历 PostToCategories
模型。
如果您的数据库提供商要求表具有主键,则您必须使用显式语法,并手动创建具有主键的连接模型。这是因为 Prisma ORM(通过@relation
表示)为使用隐式语法的多对多关系创建的关系表(JOIN 表)没有主键。