TypeORM
本页面比较了 Prisma ORM 和 TypeORM。如果您想了解如何从 TypeORM 迁移到 Prisma ORM,请查看此 指南。
TypeORM 与 Prisma ORM
虽然 Prisma ORM 和 TypeORM 解决的是类似的问题,但它们的工作方式却大不相同。
TypeORM 是一种传统的 ORM,它将表映射到模型类。这些模型类可用于生成 SQL 迁移。然后,模型类的实例在运行时为应用程序提供 CRUD 查询的接口。
Prisma ORM 是一种新型的 ORM,它可以缓解传统 ORM 的许多问题,例如臃肿的模型实例、将业务逻辑与存储逻辑混合、缺乏类型安全或由延迟加载等原因导致的不可预测的查询。
它使用 Prisma 模式 以声明方式定义应用程序模型。然后,Prisma Migrate 允许根据 Prisma 模式生成 SQL 迁移并将其执行到数据库中。CRUD 查询由 Prisma Client 提供,Prisma Client 是一个轻量级且完全类型安全的 Node.js 和 TypeScript 数据库客户端。
API 设计和抽象级别
TypeORM 和 Prisma ORM 在不同的抽象级别上运行。TypeORM 更接近于在其 API 中镜像 SQL,而 Prisma Client 提供了一个更高级别的抽象,该抽象经过精心设计,考虑了应用程序开发人员的常见任务。Prisma ORM 的 API 设计严重依赖于 使正确的事情变得简单 的理念。
虽然 Prisma Client 在更高级别的抽象上运行,但它力求公开底层数据库的全部功能,允许您在任何时候降级到 原始 SQL,如果您的用例需要的话。
以下部分将检查一些示例,说明 Prisma ORM 和 TypeORM 的 API 在某些场景下的差异以及这些情况下 Prisma ORM 的 API 设计的基本原理。
过滤
TypeORM 主要依赖 SQL 运算符来过滤列表或记录,例如使用 find
方法。另一方面,Prisma ORM 提供了一组更 通用的运算符,这些运算符使用起来很直观。还应该注意的是,如类型安全部分 下面 所述,TypeORM 在许多场景下都会在过滤器查询中丢失类型安全。
查看 string
过滤器是一个很好的例子,说明 TypeORM 和 Prisma ORM 的过滤 API 如何不同。虽然 TypeORM 主要提供基于 ILike
运算符的过滤器(直接来自 SQL),但 Prisma ORM 提供了开发人员可以使用的更具体的运算符,例如:contains
、startsWith
和 endsWith
。
const posts = await prisma.post.findMany({
where: {
title: 'Hello World',
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { contains: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('%Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { startsWith: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { endsWith: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('%Hello World'),
},
})
分页
TypeORM 仅提供 limit-offset 分页,而 Prisma ORM 方便地为 limit-offset 和基于游标的两种方法提供了专门的 API。您可以在文档的 分页 部分或 下面 的 API 比较中了解这两种方法的更多信息。
关系
在 SQL 中处理通过外键连接的记录可能会变得非常复杂。Prisma ORM 的 虚拟关系字段 概念使应用程序开发人员能够以一种直观且方便的方式处理相关数据。Prisma ORM 方法的一些好处是
- 通过流畅的 API 遍历关系(文档)
- 嵌套写入,允许更新/创建连接的记录(文档)
- 对相关记录应用过滤器(文档)
- 轻松且类型安全地查询嵌套数据,而无需担心 JOIN(文档)
- 基于模型及其关系创建嵌套的 TypeScript 类型(文档)
- 通过关系字段以直观的方式在数据模型中建模关系(文档)
- 隐式处理关系表(有时也称为 JOIN、链接、枢纽或连接表)(文档)
数据建模和迁移
Prisma 模型定义在 Prisma 模式 中,而 TypeORM 使用类和实验性的 TypeScript 装饰器来定义模型。使用 Active Record ORM 模式,TypeORM 的方法通常会导致复杂的模型实例,随着应用程序的增长,这些实例变得难以维护。
另一方面,Prisma ORM 生成一个轻量级数据库客户端,该客户端公开了一个定制的、完全类型安全的 API,用于读取和写入在 Prisma 模式中定义的模型的数据,遵循 DataMapper ORM 模式而不是 Active Record。
Prisma ORM 用于数据建模的 DSL 简洁、简单且易于使用。在 VS Code 中建模数据时,您还可以利用 Prisma ORM 强大的 VS Code 扩展,该扩展具有自动完成、快速修复、跳转到定义等功能,这些功能可以提高开发人员的生产力。
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
ManyToOne,
} from 'typeorm'
@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[]
}
@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
}
迁移在 TypeORM 和 Prisma ORM 中的工作方式类似。这两种工具都遵循根据提供的模型定义生成 SQL 文件的方法,并提供一个 CLI 来将其执行到数据库中。可以在执行迁移之前修改 SQL 文件,以便使用任一迁移系统执行任何自定义数据库操作。
类型安全
TypeORM 是 Node.js 生态系统中最早完全拥抱 TypeScript 的 ORM 之一,并且在使开发人员能够为其数据库查询获得一定程度的类型安全方面做得非常出色。
但是,在许多情况下,TypeORM 的类型安全保证都达不到要求。以下部分描述了 Prisma ORM 可以为查询结果的类型提供更强保证的场景。
选择字段
本部分说明在查询中选择模型字段子集时类型安全的差异。
TypeORM
TypeORM 为其 find
方法(例如 find
、findByIds
、findOne
等)提供了 select
选项,例如
- `find` 带 `select`
- 模型
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
select: ['id', 'title'],
})
@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
}
虽然返回的 publishedPosts
数组中的每个对象在运行时仅包含选定的 id
和 title
属性,但 TypeScript 编译器对此一无所知。它允许您在查询后访问 Post
实体上定义的任何其他属性,例如
const post = publishedPosts[0]
// The TypeScript compiler has no issue with this
if (post.content.length > 0) {
console.log(`This post has some content.`)
}
此代码将在运行时导致错误
TypeError: Cannot read property 'length' of undefined
TypeScript 编译器只看到返回对象的 Post
类型,但它不知道这些对象在运行时实际包含哪些字段。因此,它无法保护您免受访问在数据库查询中未检索到的字段,从而导致运行时错误。
Prisma ORM
Prisma Client 可以在相同情况下保证完全的类型安全,并防止您访问未从数据库中检索到的字段。
考虑使用 Prisma Client 查询的相同示例
- `findMany` 带 `select`
- 模型
const publishedPosts = await prisma.post.findMany({
where: { published: true },
select: {
id: true,
title: true,
},
})
const post = publishedPosts[0]
// The TypeScript compiler will not allow this
if (post.content.length > 0) {
console.log(`This post has some content.`)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
在这种情况下,TypeScript 编译器将在编译时抛出以下错误
[ERROR] 14:03:39 ⨯ Unable to compile TypeScript:
src/index.ts:36:12 - error TS2339: Property 'content' does not exist on type '{ id: number; title: string; }'.
42 if (post.content.length > 0) {
这是因为 Prisma Client动态生成其查询的返回类型。在这种情况下,publishedPosts
的类型如下
const publishedPosts: {
id: number
title: string
}[]
因此,您不可能意外地访问在查询中未检索到的模型上的属性。
加载关系
本部分说明在查询中加载模型的关系时类型安全的差异。在传统的 ORM 中,这有时称为急切加载。
TypeORM
TypeORM 允许通过传递给其 find
方法的 relations
选项,从数据库中急切加载关系。
考虑以下示例
- 带
relations
的find
- 模型
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
relations: ['author'],
})
@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
}
@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[]
}
与 select
不同,TypeORM 不提供传递给 relations
选项的字符串的自动完成功能,也不提供任何类型安全。这意味着 TypeScript 编译器无法捕获查询这些关系时发生的任何拼写错误。例如,它允许以下查询
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
// this query would lead to a runtime error because of a typo
relations: ['authors'],
})
这种细微的拼写错误现在会导致以下运行时错误
UnhandledPromiseRejectionWarning: Error: Relation "authors" was not found; please check if it is correct and really exists in your entity.
Prisma ORM
Prisma ORM 可以保护您免受此类错误的影响,从而消除应用程序在运行时可能发生的一整类错误。当使用 include
在 Prisma Client 查询中加载关系时,您不仅可以利用自动完成功能来指定查询,而且查询结果也会被正确地类型化
- 带
relations
的find
- 模型
const publishedPosts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
})
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
同样,publishedPosts
的类型是动态生成的,如下所示
const publishedPosts: (Post & {
author: User
})[]
作为参考,以下是 Prisma Client 为您的 Prisma 模型生成的 User
和 Post
类型
- `User`
- `Post`
// Generated by Prisma ORM
export type User = {
id: number
name: string | null
email: string
}
// Generated by Prisma ORM
export type Post = {
id: number
title: string
content: string | null
published: boolean
authorId: number | null
}
过滤
本节说明使用 where
过滤记录列表时类型安全性的差异。
TypeORM
TypeORM 允许将 where
选项传递给其 find
方法,以根据特定条件过滤返回的记录列表。这些条件可以根据模型的属性定义。
使用运算符丢失类型安全性
考虑以下示例
- `find` 带 `select`
- 模型
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan(0),
},
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ nullable: true })
views: number
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
此代码正常运行并在运行时生成有效的查询。但是,where
选项在各种不同的场景中并不是真正类型安全的。当使用仅适用于特定类型(ILike
适用于字符串,MoreThan
适用于数字)的 FindOperator
(如 ILike
或 MoreThan
)时,您将失去为模型字段提供正确类型的保证。
例如,您可以向 MoreThan
运算符提供一个字符串。TypeScript 编译器不会报错,您的应用程序只会发生运行时错误
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan('test'),
},
})
上面的代码会导致一个运行时错误,而 TypeScript 编译器不会为您捕获它
error: error: invalid input syntax for type integer: "test"
指定不存在的属性
另请注意,TypeScript 编译器允许您在 where
选项中指定模型中不存在的属性——再次导致运行时错误
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
viewCount: 1,
},
})
在这种情况下,您的应用程序在运行时再次出现以下错误
EntityColumnNotFound: No entity column "viewCount" was found.
Prisma ORM
在类型安全方面,TypeORM 中存在问题的这两种过滤场景都由 Prisma ORM 以完全类型安全的方式涵盖。
运算符的类型安全用法
使用 Prisma ORM,TypeScript 编译器会强制执行每个字段的运算符的正确用法
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
views: { gt: 0 },
},
})
不允许使用 Prisma Client 指定上面显示的相同问题查询
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
views: { gt: 'test' }, // Caught by the TypeScript compiler
},
})
TypeScript 编译器会捕获此错误并抛出以下错误,以防止应用程序发生运行时故障
[ERROR] 16:13:50 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ gt: string; }' is not assignable to type 'number | IntNullableFilter'.
Type '{ gt: string; }' is not assignable to type 'IntNullableFilter'.
Types of property 'gt' are incompatible.
Type 'string' is not assignable to type 'number'.
42 views: { gt: "test" }
作为模型属性的过滤器的类型安全定义
使用 TypeORM,您可以在 where
选项中指定一个不映射到模型字段的属性。在上面的示例中,过滤 viewCount
因此导致运行时错误,因为该字段实际上称为 views
。
使用 Prisma ORM,TypeScript 编译器不允许引用 where
内的任何模型中不存在的属性
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
viewCount: { gt: 0 }, // Caught by the TypeScript compiler
},
})
同样,TypeScript 编译器会显示以下消息以防止您犯错
[ERROR] 16:16:16 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ published: boolean; title: { contains: string; }; viewCount: { gt: number; }; }' is not assignable to type 'PostWhereInput'.
Object literal may only specify known properties, and 'viewCount' does not exist in type 'PostWhereInput'.
42 viewCount: { gt: 0 }
创建新记录
本节说明创建新记录时类型安全性的差异。
TypeORM
使用 TypeORM,有两种主要方法可以在数据库中创建新记录:insert
和 save
。这两种方法都允许开发人员提交可能导致运行时错误的数据,前提是未提供必需字段。
考虑以下示例
- 使用 `save` 创建
- 使用 `insert` 创建
- 模型
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)
const userRepository = getManager().getRepository(User)
userRepository.insert({
name: 'Alice',
})
@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[]
}
无论您是使用 TypeORM 的 save
还是 insert
进行记录创建,如果您忘记提供必需字段的值,您都会收到以下运行时错误
QueryFailedError: null value in column "email" of relation "user" violates not-null constraint
email
字段在 User
实体中定义为必需(由数据库中的 NOT NULL
约束强制执行)。
Prisma ORM
Prisma ORM 通过强制您提交模型所有必需字段的值来保护您免受此类错误的影响。
例如,以下尝试创建一个新的 User
,其中缺少必需的 email
字段,将被 TypeScript 编译器捕获
- 使用 `create` 创建
- 模型
const newUser = await prisma.user.create({
data: {
name: 'Alice',
},
})
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
}
这将导致以下编译时错误
[ERROR] 10:39:07 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2741: Property 'email' is missing in type '{ name: string; }' but required in type 'UserCreateInput'.
API 比较
获取单个对象
Prisma ORM
const user = await prisma.user.findUnique({
where: {
id: 1,
},
})
TypeORM
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)
获取单个对象的选定标量
Prisma ORM
const user = await prisma.user.findUnique({
where: {
id: 1,
},
select: {
name: true,
},
})
TypeORM
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
select: ['id', 'email'],
})
获取关系
Prisma ORM
- 使用 include
- Fluent API
const posts = await prisma.user.findUnique({
where: {
id: 2,
},
include: {
post: true,
},
})
const posts = await prisma.user
.findUnique({
where: {
id: 2,
},
})
.post()
注意:
select
返回一个包含post
数组的user
对象,而 Fluent API 仅返回一个post
数组。
TypeORM
- 使用 `relations`
- 使用 `JOIN`
- 使用急切关系
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
relations: ['posts'],
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
join: {
alias: 'user',
leftJoinAndSelect: {
posts: 'user.posts',
},
},
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)
过滤具体值
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: {
contains: 'Hello',
},
},
})
TypeORM
const userRepository = getRepository(User)
const users = await userRepository.find({
where: {
name: 'Alice',
},
})
其他过滤条件
Prisma ORM
Prisma ORM 生成许多 其他过滤器,这些过滤器通常用于现代应用程序开发。
TypeORM
TypeORM 提供 内置运算符,可用于创建更复杂的比较
关系过滤器
Prisma ORM
Prisma ORM 允许您根据不仅适用于要检索的列表的模型,而且适用于该模型的关系的条件来过滤列表。
例如,以下查询返回标题中包含“Hello”的一个或多个帖子的用户
const posts = await prisma.user.findMany({
where: {
Post: {
some: {
title: {
contains: 'Hello',
},
},
},
},
})
TypeORM
TypeORM 没有提供用于关系过滤器的专用 API。您可以通过使用 QueryBuilder
或手动编写查询来获得类似的功能。
分页
Prisma ORM
游标式分页
const page = await prisma.post.findMany({
before: {
id: 242,
},
last: 20,
})
偏移分页
const cc = await prisma.post.findMany({
skip: 200,
first: 20,
})
TypeORM
const postRepository = getRepository(Post)
const posts = await postRepository.find({
skip: 5,
take: 10,
})
创建对象
Prisma ORM
const user = await prisma.user.create({
data: {
email: '[email protected]',
},
})
TypeORM
- 使用 `save`
- 使用 `create`
- 使用 `insert`
const user = new User()
user.name = 'Alice'
user.email = '[email protected]'
await user.save()
const userRepository = getRepository(User)
const user = await userRepository.create({
name: 'Alice',
email: '[email protected]',
})
await user.save()
const userRepository = getRepository(User)
await userRepository.insert({
name: 'Alice',
email: '[email protected]',
})
更新对象
Prisma ORM
const user = await prisma.user.update({
data: {
name: 'Alicia',
},
where: {
id: 2,
},
})
TypeORM
const userRepository = getRepository(User)
const updatedUser = await userRepository.update(id, {
name: 'James',
email: '[email protected]',
})
删除对象
Prisma ORM
const deletedUser = await prisma.user.delete({
where: {
id: 10,
},
})
TypeORM
- 使用 `delete`
- 使用 `remove`
const userRepository = getRepository(User)
await userRepository.delete(id)
const userRepository = getRepository(User)
const deletedUser = await userRepository.remove(user)
批量更新
Prisma ORM
const user = await prisma.user.updateMany({
data: {
name: 'Published author!',
},
where: {
Post: {
some: {
published: true,
},
},
},
})
TypeORM
您可以使用 查询构建器更新数据库中的实体。
批量删除
Prisma ORM
const users = await prisma.user.deleteMany({
where: {
id: {
in: [1, 2, 6, 6, 22, 21, 25],
},
},
})
TypeORM
- 使用 `delete`
- 使用 `remove`
const userRepository = getRepository(User)
await userRepository.delete([id1, id2, id3])
const userRepository = getRepository(User)
const deleteUsers = await userRepository.remove([user1, user2, user3])
事务
Prisma ORM
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Bob Rufus',
Post: {
create: [
{ title: 'Working at Prisma' },
{ title: 'All about databases' },
],
},
},
})
TypeORM
await getConnection().$transaction(async (transactionalEntityManager) => {
const user = getRepository(User).create({
name: 'Bob',
email: '[email protected]',
})
const post1 = getRepository(Post).create({
title: 'Join us for GraphQL Conf in 2019',
})
const post2 = getRepository(Post).create({
title: 'Subscribe to GraphQL Weekly for GraphQL news',
})
user.posts = [post1, post2]
await transactionalEntityManager.save(post1)
await transactionalEntityManager.save(post2)
await transactionalEntityManager.save(user)
})