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 提供了開發人員可以使用的更具體的運算符,例如:contains
、startsWith
和 endsWith
。
const posts = await prisma.post.findMany({
where: {
title: 'Hello World',
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { contains: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('%Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { startsWith: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { endsWith: 'Hello World' },
},
})
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 擴充功能,其功能包括自動完成、快速修復、跳到定義以及其他提高開發人員生產力的優勢。
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])
}
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
選項,例如
- `find` 與 `select`
- 模型
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
select: ['id', 'title'],
})
@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
}
雖然傳回的 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 查詢的相同範例
- `findMany` 與 `select`
- 模型
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.`)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
在這種情況下,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
選項從資料庫預先載入關聯。
考慮這個範例
- `find` 與 `relations`
- 模型
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
relations: ['author'],
})
@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
}
@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[]
}
與 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 查詢中載入關聯時,您不僅可以利用自動完成來指定查詢,而且查詢的結果也將被正確地鍵入
- `find` 與 `relations`
- 模型
const publishedPosts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
})
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])
}
同樣,publishedPosts
的類型是即時生成的,如下所示
const publishedPosts: (Post & {
author: User
})[]
作為參考,這是 Prisma Client 為您的 Prisma 模型生成的 User
和 Post
類型
- `User`
- `Post`
// Generated by Prisma ORM
export type User = {
id: number
name: string | null
email: string
}
// Generated by Prisma ORM
export type Post = {
id: number
title: string
content: string | null
published: boolean
authorId: number | null
}
篩選
本節說明在使用 where
篩選記錄列表時類型安全性的差異。
TypeORM
TypeORM 允許將 where
選項傳遞給其 find
方法,以根據特定條件篩選傳回的記錄列表。這些條件可以相對於模型的屬性來定義。
使用運算符時失去類型安全
考慮這個範例
- `find` 與 `select`
- 模型
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan(0),
},
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ nullable: true })
views: number
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
此程式碼可以正常運行,並在運行時產生有效的查詢。但是,在各種不同的情況下,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
在類型安全性方面,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,有兩種主要方法可以在資料庫中建立新記錄:insert
和 save
。當未提供必填欄位時,這兩種方法都允許開發人員提交可能導致運行時錯誤的資料。
考慮這個範例
- 使用 `save` 建立
- 使用 `insert` 建立
- 模型
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)
const userRepository = getManager().getRepository(User)
userRepository.insert({
name: 'Alice',
})
@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[]
}
無論您是使用 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 編譯器捕獲
- 使用 `create` 建立
- 模型
const newUser = await prisma.user.create({
data: {
name: 'Alice',
},
})
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
}
它會導致以下編譯時錯誤
[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
- 使用 include
- 流暢 API
const posts = await prisma.user.findUnique({
where: {
id: 2,
},
include: {
post: true,
},
})
const posts = await prisma.user
.findUnique({
where: {
id: 2,
},
})
.post()
注意:
select
返回一個包含post
陣列的user
物件,而流暢 API 僅返回post
陣列。
TypeORM
- 使用 `relations`
- 使用 `JOIN`
- 使用預先載入關聯
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
relations: ['posts'],
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
join: {
alias: 'user',
leftJoinAndSelect: {
posts: 'user.posts',
},
},
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)
篩選特定值
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
- 使用 `save`
- 使用 `create`
- 使用 `insert`
const user = new User()
user.name = 'Alice'
user.email = 'alice@prisma.io'
await user.save()
const userRepository = getRepository(User)
const user = await userRepository.create({
name: 'Alice',
email: 'alice@prisma.io',
})
await user.save()
const userRepository = getRepository(User)
await userRepository.insert({
name: 'Alice',
email: 'alice@prisma.io',
})
更新物件
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
- 使用 `delete`
- 使用 `remove`
const userRepository = getRepository(User)
await userRepository.delete(id)
const userRepository = getRepository(User)
const deletedUser = await userRepository.remove(user)
批次更新
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
- 使用 `delete`
- 使用 `remove`
const userRepository = getRepository(User)
await userRepository.delete([id1, id2, id3])
const userRepository = getRepository(User)
const deleteUsers = await userRepository.remove([user1, user2, user3])
事務
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)
})