TypeORM
本页比较了 Prisma ORM 和 TypeORM。如果您想了解如何从 TypeORM 迁移到 Prisma ORM,请查看此指南。
TypeORM vs Prisma ORM
虽然 Prisma ORM 和 TypeORM 解决了类似的问题,但它们的工作方式截然不同。
TypeORM 是一个传统的 ORM,它将表映射到模型类。这些模型类可用于生成 SQL 迁移。然后,模型类的实例在运行时为应用程序提供 CRUD 查询的接口。
Prisma ORM 是一种新型 ORM,它缓解了传统 ORM 的许多问题,例如臃肿的模型实例、业务逻辑与存储逻辑的混合、缺乏类型安全或由延迟加载等引起的不可预测的查询。
它使用 Prisma schema 以声明方式定义应用程序模型。然后,Prisma Migrate 允许从 Prisma schema 生成 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 在许多场景下的过滤器查询中会丢失类型安全性。
TypeORM 和 Prisma ORM 的过滤 API 如何不同的一个很好的例子是查看 string
过滤器。虽然 TypeORM 主要提供基于直接来自 SQL 的 ILike
运算符的过滤器,但 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 schema 中定义,而 TypeORM 使用类和实验性的 TypeScript 装饰器进行模型定义。使用 Active Record ORM 模式,TypeORM 的方法通常会导致复杂的模型实例,随着应用程序的增长,这些实例变得难以维护。
另一方面,Prisma ORM 生成一个轻量级数据库客户端,该客户端公开一个定制且完全类型安全的 API,用于读取和写入在 Prisma schema 中定义的模型的数据,遵循 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
选项从数据库中急切加载关系。
考虑以下示例
- `find` 与 `relations`
- 模型
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 查询中加载关系时,您不仅可以利用自动完成来指定查询,而且查询的结果也将被正确类型化
- `find` 与 `relations`
- 模型
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
选项在各种不同的场景中实际上不是类型安全的。当使用仅适用于特定类型的 FindOperator
(例如 ILike
或 MoreThan
)(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[]
}
无论您是使用 save
还是 insert
在 TypeORM 中创建记录,如果您忘记为必填字段提供值,您都将收到以下运行时错误
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
- 流畅的 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
对象,而流畅的 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)
})