跳到主要內容

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 提供了開發人員可以使用的更具體的運算符,例如: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 僅提供 limit-offset 分頁,而 Prisma ORM 方便地為 limit-offset 和基於游標的分頁都提供了專用的 API。您可以在文件中的 分頁 章節或 API 比較 下方 中了解有關這兩種方法的更多資訊。

關聯

在 SQL 中,處理透過外鍵連線的記錄可能會變得非常複雜。Prisma ORM 的 虛擬關聯欄位 概念為應用程式開發人員提供了一種直觀且方便的方式來處理相關資料。Prisma ORM 方法的一些優點是

  • 透過流暢的 API 遍歷關係 (文件)
  • 巢狀寫入,可以更新/建立連線的記錄 (文件)
  • 對相關記錄應用篩選器 (文件)
  • 輕鬆且類型安全地查詢巢狀資料,而無需擔心 JOIN (文件)
  • 根據模型及其關係建立巢狀 TypeScript 類型定義 (文件)
  • 透過關聯欄位在資料模型中直觀地建模關係 (文件)
  • 隱式處理關係表格(有時也稱為 JOIN、link、pivot 或 junction 表格)(文件)

資料建模與遷移

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 方法(例如 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 允許將 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(例如 ILikeMoreThanILike 適用於字串,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)
})