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 客户端提供,Prisma 客户端是针对 Node.js 和 TypeScript 的轻量级且完全类型安全的数据库客户端。
API 设计和抽象级别
TypeORM 和 Prisma ORM 运行在不同的抽象级别上。 TypeORM 更加接近于在 API 中镜像 SQL,而 Prisma 客户端提供了一个更高级别的抽象,该抽象经过精心设计,以满足应用程序开发人员的常见任务。 Prisma ORM 的 API 设计在很大程度上依赖于 使正确的事情变得容易 的理念。
虽然 Prisma 客户端运行在更高抽象级别上,但它力求公开底层数据库的全部功能,允许您在需要时随时下降到 原始 SQL。
以下部分将检查几个示例,说明 Prisma ORM 和 TypeORM 的 API 在某些情况下如何不同,以及 Prisma ORM 的 API 设计在这些情况下的基本原理是什么。
过滤
TypeORM 主要依赖于 SQL 运算符来过滤列表或记录,例如使用 find
方法。 另一方面,Prisma ORM 提供了一组更 通用的运算符,这些运算符使用起来很直观。 还应该注意的是,如类型安全部分 所述,TypeORM 在许多情况下会在过滤查询中失去类型安全。
一个很好的例子是比较 TypeORM 和 Prisma ORM 的过滤 API 如何不同,方法是查看 string
过滤器。 虽然 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 仅提供限制偏移分页,而 Prisma ORM 方便地提供了针对限制偏移和基于游标的分页的专用 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` with `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 客户端可以在相同情况下保证完全类型安全,并保护您免受访问未从数据库中检索到的字段。
考虑使用 Prisma 客户端查询的相同示例
- `findMany` with `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 客户端动态生成其查询的返回类型。 在这种情况下,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` with `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[]
}
无论您使用 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
- 流畅 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)
})