跳到主内容

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 主要提供基于 `ILike` 运算符的过滤器,该运算符直接来自 SQL,而 Prisma ORM 则提供了开发人员可以使用更具体的运算符,例如:`contains`、`startsWith` 和 `endsWith`。

Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: 'Hello World',
},
})
TypeORM
const posts = await postRepository.find({
where: {
title: ILike('Hello World'),
},
})
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: { contains: 'Hello World' },
},
})
TypeORM
const posts = await postRepository.find({
where: {
title: ILike('%Hello World%'),
},
})
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: { startsWith: 'Hello World' },
},
})
TypeORM
const posts = await postRepository.find({
where: {
title: ILike('Hello World%'),
},
})
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: { endsWith: 'Hello World' },
},
})
TypeORM
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 扩展,该扩展具有自动补全、快速修复、跳转到定义等功能,以及其他提高开发人员生产力的优势。

Prisma ORM
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])
}
TypeORM
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` 选项,例如

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
select: ['id', 'title'],
})

虽然返回的 `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 查询的相同示例

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.`)
}

在这种情况下,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 中,这有时称为预加载(eager loading)。

TypeORM

TypeORM 允许通过传递给其 `find` 方法的 `relations` 选项从数据库预加载关系。

考虑这个例子

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
relations: ['author'],
})

与 `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 查询中加载关系时,您不仅可以利用自动补全来指定查询,而且查询结果也将具有正确的类型。

const publishedPosts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
})

同样,`publishedPosts` 的类型是动态生成的,如下所示

const publishedPosts: (Post & {
author: User
})[]

作为参考,这是 Prisma Client 为您的 Prisma 模型生成的 `User` 和 `Post` 类型示例

// Generated by Prisma ORM
export type User = {
id: number
name: string | null
email: string
}

过滤

本节解释了使用 `where` 过滤记录列表时类型安全的差异。

TypeORM

TypeORM 允许将 `where` 选项传递给其 `find` 方法,以根据特定标准过滤返回的记录列表。这些标准可以针对模型的属性进行定义。

使用运算符丢失类型安全

考虑这个例子

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan(0),
},
})

此代码正常运行并在运行时生成有效的查询。但是,`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

Prisma ORM 以完全类型安全的方式涵盖了 TypeORM 在类型安全方面存在的两个过滤场景问题。

运算符的类型安全使用

使用 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`。这两种方法都允许开发人员提交数据,当未提供必需字段时,这可能导致运行时错误。

考虑这个例子

const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)

无论您是使用 TypeORM 的 `save` 还是 `insert` 创建记录,如果您忘记为必需字段提供值,都将收到以下运行时错误

QueryFailedError: null value in column "email" of relation "user" violates not-null constraint

`email` 字段在 `User` 实体上被定义为必需字段(这由数据库中的 `NOT NULL` 约束强制执行)。

Prisma ORM

Prisma ORM 通过强制要求您为模型的所有必需字段提交值来保护您免受此类错误的影响。

例如,以下尝试创建一个缺失必需 `email` 字段的新 `User` 将被 TypeScript 编译器捕获

const newUser = await prisma.user.create({
data: {
name: 'Alice',
},
})

它将导致以下编译时错误

[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

const posts = await prisma.user.findUnique({
where: {
id: 2,
},
include: {
post: true,
},
})

**注意**:`select` 返回一个包含 `post` 数组的 `user` 对象,而流式 API 只返回一个 `post` 数组。

TypeORM

const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
relations: ['posts'],
})

按具体值过滤

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: 'alice@prisma.io',
},
})

TypeORM

const user = new User()
user.name = 'Alice'
user.email = 'alice@prisma.io'
await user.save()

更新对象

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: 'james@prisma.io',
})

删除对象

Prisma ORM

const deletedUser = await prisma.user.delete({
where: {
id: 10,
},
})

TypeORM

const userRepository = getRepository(User)
await userRepository.delete(id)

批量更新

Prisma ORM

const user = await prisma.user.updateMany({
data: {
name: 'Published author!',
},
where: {
Post: {
some: {
published: true,
},
},
},
})

TypeORM

您可以使用query builder 更新数据库中的实体

批量删除

Prisma ORM

const users = await prisma.user.deleteMany({
where: {
id: {
in: [1, 2, 6, 6, 22, 21, 25],
},
},
})

TypeORM

const userRepository = getRepository(User)
await userRepository.delete([id1, id2, id3])

事务

Prisma ORM

const user = await prisma.user.create({
data: {
email: 'bob.rufus@prisma.io',
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: 'bob@prisma.io',
})
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)
})

与 Prisma 保持联系

通过联系以下方式继续您的 Prisma 之旅: 我们的活跃社区。及时了解信息,参与其中,并与其他开发者协作

我们非常珍视您的参与,并期待您成为我们社区的一员!