跳到主要內容

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 之旅: 我們活躍的社群。保持資訊暢通,積極參與,與其他開發者協作。

我們真誠地重視你的參與,並期待你成為我們社群的一員!

© . This site is unofficial and not affiliated with Prisma Data, Inc.