跳到主要内容

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 提供,这是一个轻量级且完全类型安全的 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 差异的一个很好例子是字符串过滤器。TypeORM 主要提供基于直接来自 SQL 的 ILike 运算符的过滤器,而 Prisma ORM 提供开发人员可以使用的更具体的运算符,例如:containsstartsWithendsWith

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 仅提供限制-偏移分页,而 Prisma ORM 则方便地提供了限制-偏移分页和基于游标分页的专用 API。你可以在文档的分页部分或下方的 API 比较中了解这两种方法的更多信息。

关系

在 SQL 中,处理通过外键连接的记录可能会变得非常复杂。Prisma ORM 的虚拟关系字段概念为应用程序开发人员提供了一种直观便捷的方式来处理相关数据。Prisma ORM 方法的一些优点包括:

  • 通过 fluent API 遍历关系(文档
  • 启用更新/创建连接记录的嵌套写入(文档
  • 对相关记录应用过滤器(文档
  • 轻松且类型安全地查询嵌套数据,无需担心 JOIN(文档
  • 基于模型及其关系创建嵌套 TypeScript 类型(文档
  • 通过关系字段在数据模型中直观地建模关系(文档
  • 隐式处理关系表(有时也称为 JOIN、link、pivot 或 junction 表)(文档

数据建模和迁移

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 扩展,其功能包括自动补全、快速修复、跳转定义以及其他可提高开发人员生产力的优点。

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 方法(例如 findfindByIdsfindOne 等)提供了 select 选项,例如:

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

尽管返回的 publishedPosts 数组中的每个对象在运行时只包含选定的 idtitle 属性,但 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 中,这有时被称为急切加载

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 模型生成的 UserPost 类型的外观:

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

过滤

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

TypeORM

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

使用运算符时失去类型安全

考虑这个例子:

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

此代码正常运行并在运行时生成有效查询。然而,where 选项在各种不同场景下并非真正类型安全。当使用 FindOperator(例如 ILikeMoreThan)时,它们仅适用于特定类型(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,有两种主要方式在数据库中创建新记录:insertsave。这两种方法都允许开发人员提交数据,当未提供必需字段时,这可能会导致运行时错误。

考虑这个例子:

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

无论你使用 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 编译器捕获:

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

你可以使用查询构建器来更新数据库中的实体

批量删除

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 之旅: 我们活跃的社区。保持信息畅通,积极参与,与其他开发者协作。

我们真诚地重视你的参与,并期待你成为我们社区的一员!

© . All rights reserved.