跳至主要內容

TypeORM

本頁面比較了 Prisma ORM 與 TypeORM。如果您想了解如何從 TypeORM 遷移到 Prisma ORM,請查看此指南

TypeORM 與 Prisma ORM

雖然 Prisma ORM 和 TypeORM 解決的問題相似,但它們的運作方式卻截然不同。

TypeORM 是一個傳統的 ORM,它將資料表(tables)映射為模型類別(model classes)。這些模型類別可用於產生 SQL 遷移檔。在執行時期(runtime),這些模型類別的實例會為應用程式提供 CRUD 查詢的介面。

Prisma ORM 是一種新型態的 ORM,它減輕了傳統 ORM 的許多問題,例如臃腫的模型實例、將業務邏輯與儲存邏輯混雜、缺乏型別安全,或是由延遲載入(lazy loading)等原因導致的不可預測查詢。

它使用 Prisma schema 以宣告式的方式定義應用程式模型。接著,Prisma Migrate 允許根據 Prisma schema 產生 SQL 遷移檔,並在資料庫執行它們。CRUD 查詢則由 Prisma Client 提供,這是一個輕量級且完全型別安全的 Node.js 與 TypeScript 資料庫客戶端。

API 設計與抽象層級

TypeORM 和 Prisma ORM 操作在不同的抽象層級。TypeORM 的 API 更接近於鏡像 SQL,而 Prisma Client 則提供了更高層級的抽象,且在設計時充分考慮了應用程式開發人員的常見需求。Prisma ORM 的 API 設計理念深受「讓正確的事情變得簡單」(making the right thing easy)這一理念的影響。

雖然 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'),
},
})

分頁(Pagination)

TypeORM 僅提供 limit-offset 分頁,而 Prisma ORM 則為 limit-offset 和基於游標(cursor-based)的分頁皆提供了專用的 API。您可以在文件的分頁章節或下方的 API 比較中,進一步了解這兩種方法。

關聯

在 SQL 中處理透過外鍵連接的記錄可能會變得非常複雜。Prisma ORM 的虛擬關聯欄位(virtual relation field)概念,為應用程式開發人員處理相關資料提供了一種直覺且方便的方法。Prisma ORM 的一些優勢包括:

  • 透過 Fluent API 遍歷關聯(文件
  • 支援更新/建立關聯記錄的巢狀寫入(文件
  • 對關聯記錄應用過濾器(文件
  • 輕鬆且型別安全地查詢巢狀資料,無需擔心 JOIN(文件
  • 基於模型及其關聯建立巢狀的 TypeScript 型別定義(文件
  • 透過關聯欄位在資料模型中直覺地建立關聯模型(文件
  • 隱含處理關聯資料表(有時也稱為 JOIN、連結、樞紐或連接資料表)(文件

資料建模與遷移

Prisma 模型定義在 Prisma schema 中,而 TypeORM 使用類別和實驗性的 TypeScript 裝飾器(decorators)來進行模型定義。採用 Active Record ORM 模式的 TypeORM,其方法通常會導致複雜的模型實例,隨著應用程式成長而變得難以維護。

另一方面,Prisma ORM 會產生一個輕量級的資料庫客戶端,該客戶端為 Prisma schema 中定義的模型公開了一組量身打造且完全型別安全的讀寫 API,遵循 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 陣列中的每個物件在執行時期僅包含選取的 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 即時(on the fly)產生其查詢的回傳型別。在本例中,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 可以保護您免受這類錯誤的影響,從而消除應用程式在執行時期可能發生的一整類錯誤。當在 Prisma Client 查詢中使用 include 來載入關聯時,您不僅可以利用自動補全功能來指定查詢,查詢結果也會被正確地型別化:

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
}

過濾(Filtering)

本章節說明使用 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 選項在多種不同場景下並非真正的型別安全。當使用僅適用於特定型別(ILike 適用於字串,MoreThan 適用於數字)的 FindOperator(如 ILikeMoreThan)時,您將失去為模型欄位提供正確型別的保證。

例如,您可以為 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。這兩種方法都允許開發人員提交資料,當必需(required)欄位未提供時,可能會導致執行時期錯誤。

考慮以下範例:

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 透過強制您提交模型所有必需欄位的值,來保護您免受這類錯誤的影響。

例如,以下嘗試建立一個缺少必需 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)

取得單一物件的選定標量(Scalars)

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 物件,而 Fluent 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 提供了可用於建立更複雜比較的內建運算子

關聯篩選器 (Relation filters)

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,
})

偏移量(Offset)分頁

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])

事務 (Transactions)

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.