共计 8767 个字符,预计需要花费 22 分钟才能阅读完成。
ORM(Object relational mappers) 的含意是,将数据模型与 Object 建设强力的映射关系,这样咱们对数据的增删改查能够转换为操作 Object(对象)。
Prisma 是一个古代 Nodejs ORM 库,依据 Prisma 官网文档 能够理解这个库是如何设计与应用的。
概述
Prisma 提供了大量工具,包含 Prisma Schema、Prisma Client、Prisma Migrate、Prisma CLI、Prisma Studio 等,其中最外围的两个是 Prisma Schema 与 Prisma Client,别离是形容利用数据模型与 Node 操作 API。
与个别 ORM 齐全由 Class 形容数据模型不同,Primsa 采纳了一个全新语法 Primsa Schema 形容数据模型,再执行 prisma generate
产生一个配置文件存储在 node_modules/.prisma/client
中,Node 代码里就能够应用 Prisma Client 对数据增删改查了。
Prisma Schema
Primsa Schema 是在最大水平贴近数据库构造形容的根底上,对关联关系进行了进一步形象,并且背地保护了与数据模型的对应关系,下图很好的阐明了这一点:
<img width=400 src=”https://z3.ax1x.com/2021/10/17/5YwZoF.png”>
能够看到,简直与数据库的定义截然不同,惟一多进去的 posts
与 author
其实是补救了数据库表关联外键中不直观的局部,将这些外键转化为实体对象,让操作时感触不到外键或者多表的存在,在具体操作时再转化为 join 操作。上面是对应的 Prisma Schema:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {provider = "prisma-client-js"}
model Post {id Int @id @default(autoincrement())
title String
content String? @map("post_content")
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]}
datasource db
申明了链接数据库信息;generator client
申明了应用 Prisma Client 进行客户端操作,也就是说 Prisma Client 其实是能够替换实现的;model
是最外围的模型定义。
在模型定义中,能够通过 @map
批改字段名映射、@@map
批改表名映射,默认状况下,字段名与 key 名雷同:
model Comment {title @map("comment_title")
@@map("comments")
}
字段由上面四种形容组成:
- 字段名。
- 字段类型。
- 可选的类型润饰。
- 可选的属性形容。
model Tag {name String? @id}
在这个形容里,蕴含字段名 name
、字段类型 String
、类型润饰 ?
、属性形容 @id
。
字段类型
字段类型能够是 model,比方关联类型字段场景:
model Post {id Int @id @default(autoincrement())
// Other fields
comments Comment[] // A post can have many comments}
model Comment {
id Int
// Other fields
Post Post? @relation(fields: [postId], references: [id]) // A comment can have one post
postId Int?
}
关联场景有 1v1, nv1, 1vn, nvn 四种状况,字段类型能够为定义的 model 名称,并应用属性形容 @relation
定义关联关系,比方下面的例子,形容了 Commenct
与 Post
存在 nv1 关系,并且 Comment.postId
与 Post.id
关联。
字段类型还能够是底层数据类型,通过 @db.
形容,比方:
model Post {id @db.TinyInt(1)
}
对于 Prisma 不反对的类型,还能够应用 Unsupported
润饰:
model Post {someField Unsupported("polygon")?
}
这种类型的字段无奈通过 ORM API 查问,但能够通过 queryRaw
形式查问。queryRaw
是一种 ORM 对原始 SQL 模式的反对,在 Prisma Client 会提到。
类型润饰
类型润饰有 ?
[]
两种语法,比方:
model User {
name String?
posts Post[]}
别离示意可选与数组。
属性形容
属性形容有如下几种语法:
model User {id Int @id @default(autoincrement())
isAdmin Boolean @default(false)
email String @unique
@@unique([firstName, lastName])
}
@id
对应数据库的 PRIMARY KEY。
@default
设置字段默认值,能够联结函数应用,比方 @default(autoincrement())
,可用函数包含 autoincrement()
、dbgenerated()
、cuid()
、uuid()
、now()
,还能够通过 dbgenerated
间接调用数据库底层的函数,比方 dbgenerated("gen_random_uuid()")
。
@unique
设置字段值惟一。
@relation
设置关联,下面曾经提到过了。
@map
设置映射,下面也提到过了。
@updatedAt
修饰字段用来存储上次更新工夫,个别是数据库自带的能力。
@ignore
对 Prisma 标记有效的字段。
所有属性形容都能够组合应用,并且还存在需对 model 级别的形容,个别用两个 @
形容,包含 @@id
、@@unique
、@@index
、@@map
、@@ignore
。
ManyToMany
Prisma 在多对多关联关系的形容上也下了功夫,反对隐式关联形容:
model Post {id Int @id @default(autoincrement())
categories Category[]}
model Category {id Int @id @default(autoincrement())
posts Post[]}
看上去很天然,但其实背地暗藏了不少实现。数据库多对多关系个别通过第三张表实现,第三张表会存储两张表之间外键对应关系,所以如果要显式定义其实是这样的:
model Post {id Int @id @default(autoincrement())
categories CategoriesOnPosts[]}
model Category {id Int @id @default(autoincrement())
posts CategoriesOnPosts[]}
model CategoriesOnPosts {post Post @relation(fields: [postId], references: [id])
postId Int // relation scalar field (used in the `@relation` attribute above)
category Category @relation(fields: [categoryId], references: [id])
categoryId Int // relation scalar field (used in the `@relation` attribute above)
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, categoryId])
}
背地生成如下 SQL:
CREATE TABLE "Category" (id SERIAL PRIMARY KEY);
CREATE TABLE "Post" (id SERIAL PRIMARY KEY);
-- Relation table + indexes -------------------------------------------------------
CREATE TABLE "CategoryToPost" (
"categoryId" integer NOT NULL,
"postId" integer NOT NULL,
"assignedBy" text NOT NULL
"assignedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY ("categoryId") REFERENCES "Category"(id),
FOREIGN KEY ("postId") REFERENCES "Post"(id)
);
CREATE UNIQUE INDEX "CategoryToPost_category_post_unique" ON "CategoryToPost"("categoryId" int4_ops,"postId" int4_ops);
Prisma Client
形容好 Prisma Model 后,执行 prisma generate
,再利用 npm install @prisma/client
装置好 Node 包后,就能够在代码里操作 ORM 了:
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
CRUD
应用 create
创立一条记录:
const user = await prisma.user.create({
data: {
email: 'elsa@prisma.io',
name: 'Elsa Prisma',
},
})
应用 createMany
创立多条记录:
const createMany = await prisma.user.createMany({
data: [{ name: 'Bob', email: 'bob@prisma.io'},
{name: 'Bobo', email: 'bob@prisma.io'}, // Duplicate unique key!
{name: 'Yewande', email: 'yewande@prisma.io'},
{name: 'Angelique', email: 'angelique@prisma.io'},
],
skipDuplicates: true, // Skip 'Bobo'
})
应用 findUnique
查找单条记录:
const user = await prisma.user.findUnique({
where: {email: 'elsa@prisma.io',},
})
对于联结索引的状况:
model TimePeriod {
year Int
quarter Int
total Decimal
@@id([year, quarter])
}
须要再嵌套一层由 _
拼接的 key:
const timePeriod = await prisma.timePeriod.findUnique({
where: {
year_quarter: {
quarter: 4,
year: 2020,
},
},
})
应用 findMany
查问多条记录:
const users = await prisma.user.findMany()
能够应用 SQL 中各种条件语句,语法如下:
const users = await prisma.user.findMany({
where: {role: 'ADMIN',},
include: {posts: true,},
})
应用 update
更新记录:
const updateUser = await prisma.user.update({
where: {email: 'viola@prisma.io',},
data: {name: 'Viola the Magnificent',},
})
应用 updateMany
更新多条记录:
const updateUsers = await prisma.user.updateMany({
where: {
email: {contains: 'prisma.io',},
},
data: {role: 'ADMIN',},
})
应用 delete
删除记录:
const deleteUser = await prisma.user.delete({
where: {email: 'bert@prisma.io',},
})
应用 deleteMany
删除多条记录:
const deleteUsers = await prisma.user.deleteMany({
where: {
email: {contains: 'prisma.io',},
},
})
应用 include
示意关联查问是否失效,比方:
const getUser = await prisma.user.findUnique({
where: {id: 19,},
include: {posts: true,},
})
这样就会在查问 user
表时,顺带查问所有关联的 post
表。关联查问也反对嵌套:
const user = await prisma.user.findMany({
include: {
posts: {
include: {categories: true,},
},
},
})
筛选条件反对 equals
、not
、in
、notIn
、lt
、lte
、gt
、gte
、contains
、search
、mode
、startsWith
、endsWith
、AND
、OR
、NOT
,个别用法如下:
const result = await prisma.user.findMany({
where: {
name: {equals: 'Eleanor',},
},
})
这个语句代替 sql 的 where name="Eleanor"
,即通过对象嵌套的形式表白语义。
Prisma 也能够间接写原生 SQL:
const email = 'emelie@prisma.io'
const result = await prisma.$queryRaw(Prisma.sql`SELECT * FROM User WHERE email = ${email}`
)
中间件
Prisma 反对中间件的形式在执行过程中进行拓展,看上面的例子:
const prisma = new PrismaClient()
// Middleware 1
prisma.$use(async (params, next) => {console.log(params.args.data.title)
console.log('1')
const result = await next(params)
console.log('6')
return result
})
// Middleware 2
prisma.$use(async (params, next) => {console.log('2')
const result = await next(params)
console.log('5')
return result
})
// Middleware 3
prisma.$use(async (params, next) => {console.log('3')
const result = await next(params)
console.log('4')
return result
})
const create = await prisma.post.create({
data: {title: 'Welcome to Prisma Day 2020',},
})
const create2 = await prisma.post.create({
data: {title: 'How to Prisma!',},
})
输入如下:
Welcome to Prisma Day 2020
1
2
3
4
5
6
How to Prisma!
1
2
3
4
5
6
能够看到,中间件执行程序是洋葱模型,并且每个操作都会触发。咱们能够利用中间件拓展业务逻辑或者进行操作工夫的打点记录。
精读
ORM 的两种设计模式
ORM 有 Active Record 与 Data Mapper 两种设计模式,其中 Active Record 使对象背地齐全对应 sql 查问,当初曾经不怎么风行了,而 Data Mapper 模式中的对象并不知道数据库的存在,即两头多了一层映射,甚至背地不须要对应数据库,所以能够做一些很轻量的调试性能。
Prisma 采纳了 Data Mapper 模式。
ORM 容易引发性能问题
当数据量大,或者性能、资源敏感的状况下,咱们须要对 SQL 进行优化,甚至咱们须要对特定的 Mysql 的特定版本的某些内核谬误,对 SQL 进行某些看似无意义的申明调优(比方在 where 之前再进行雷同条件的 IN 范畴限定),有的时候能获得惊人的性能晋升。
而 ORM 是建设在一个较为理想化实践根底上的,即数据模型能够很好的转化为对象操作,然而对象操作因为屏蔽了细节,咱们无奈对 SQL 进行针对性调优。
另外,得益于对象操作的便利性,咱们很容易通过 obj.obj. 的形式拜访某些属性,但这背地生成的却是一系列未经优化(或者局部主动优化)的简单 join sql,咱们在写这些 sql 时会提前思考性能因素,但通过对象调用时却因为成本低,或感觉 ORM 有 magic 优化等想法,写出很多实际上不合理的 sql。
Prisma Schema 的益处
其实从语法上,Prisma Schema 与 Typeorm 基于 Class + 装璜器的拓展简直能够等价转换,但 Prisma Schema 在理论应用中有一个很不错的劣势,即缩小样板代码以及稳固数据库模型。
缩小样板代码比拟好了解,因为 Prisma Schema 并不会呈现在代码中,而稳固模型是指,只有不执行 prisma generate
,数据模型就不会变动,而且 Prisma Schema 也独立于 Node 存在,甚至能够不放在我的项目源码中,相比之下,批改起来会更加谨慎,而齐全用 Node 定义的模型因为自身是代码的一部分,可能会忽然被批改,而且也没有执行数据库构造同步的操作。
如果我的项目采纳 Prisma,则模型变更后,能够执行 prisma db pull
更新数据库构造,再执行 prisma generate
更新客户端 API,这个流程比拟清晰。
总结
Prisma Schema 是 Prisma 的一大特色,因为这部分形容独立于代码,带来了如下几个益处:
- 定义比 Node Class 更简洁。
- 不生成冗余的代码构造。
- Prisma Client 更加轻量,且查问返回的都是 Pure Object。
至于 Prisma Client 的 API 设计其实并没有特地突出之处,无论与 sequelize 还是 typeorm 的 API 设计相比,都没有太大的优化,只是格调不同。
不过对于记录的创立,我更喜爱 Prisma 的 API:
// typeorm - save API
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)
// typeorm - insert API
const userRepository = getManager().getRepository(User)
userRepository.insert({name: 'Alice',})
// sequelize
const user = User.build({name: 'Alice',})
await user.save()
// Mongoose
const user = await User.create({
name: 'Alice',
email: 'alice@prisma.io',
})
// prisma
const newUser = await prisma.user.create({
data: {name: 'Alice',},
})
首先存在 prisma
这个顶层变量,应用起来会十分不便,另外从 API 拓展上来说,尽管 Mongoose 设计得更简洁,但增加一些条件时拓展性会有余,导致构造不太稳固,不利于对立记忆。
Prisma Client 的 API 对立采纳上面这种构造:
await prisma.modelName.operateName({
// 数据,比方 create、update 时会用到
data: /** ... */,
// 条件,大部分状况都能够用到
where: /** ... */,
// 其它非凡参数,或者 operater 特有的参数
})
所以总的来说,Prisma 尽管没有对 ORM 做出革命性扭转,但在微翻新与 API 优化上都做得足够棒,github 更新也比拟沉闷,如果你决定应用 ORM 开发我的项目,还是比拟举荐 Prisma 的。
在理论应用中,为了躲避 ORM 产生蠢笨 sql 导致的性能问题,能够利用 Prisma Middleware 监控查问性能,并对性能较差的中央采纳 prisma.$queryRaw
原生 sql 查问。
探讨地址是:精读《Prisma 的应用》· Issue #362 · dt-fe/weekly
如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。
关注 前端精读微信公众号
<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>
版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)