前端精读:Prisma 使用全解析——从 Prisma Schema 到 Prisma Client 的 Node.js ORM 实战
发布时间:2026/10/2 11:51:53来源:尧图网络
文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载Prisma 是当前 Node.js 生态中极具代表性的现代 ORM 库。与传统的用 Class 描述数据模型思路不同它用一套全新的 Prisma Schema 语法描述数据模型再通过prisma generate生成类型安全的客户端代码让开发者像操作普通对象一样完成数据库增删改查。本篇以前端精读周刊中的 《Prisma 的使用》 为骨架完整拆解 Prisma Schema 的模型描述语法、Prisma Client 的 CRUD 与关联查询 API、中间件的洋葱模型执行机制并结合本仓库 SQL 系列 的集合视角说明 ORM 背后的 SQL 语义与性能取舍。读完你将能独立设计 Prisma 数据模型、编写类型安全的增删改查代码并掌握用中间件与原生 SQL 规避 ORM 性能陷阱的实战方案。Prisma 是什么现代 Node.js ORM 与它的工具集ORMObject Relational Mappers的核心含义是将数据模型与 Object 建立强力的映射关系使得对数据的增删改查可以转换为对 Object对象的操作。开发者不再直接面对 SQL 语句与表结构而是面对代码中的对象与方法调用。Prisma 就是这样一个现代 Node.js ORM 库。围绕描述数据模型、操作数据这两个诉求它提供了大量工具包括Prisma Schema一套用于描述应用数据模型的专用语法非 JavaScript/TypeScript 代码Prisma Client面向 Node 的、类型安全的数据库操作 APIPrisma Migrate数据库结构迁移工具Prisma CLI命令行工具承载prisma generate、prisma db pull、prisma migrate等命令Prisma Studio可视化查看与编辑数据的图形化工具。其中最核心的两个是Prisma Schema与Prisma Client分别承担描述数据模型与提供 Node 操作 API两个职责。与一般 ORM 完全由 Class 描述数据模型不同Prisma 采用全新的 Prisma Schema 语法描述数据模型之后执行prisma generate产生一份配置文件默认存储在node_modules/.prisma/client中Node 代码里就可以通过 Prisma Client 对数据进行增删改查了。这个Schema 描述 → 生成客户端的流程是理解 Prisma 一切设计的关键起点。Prisma Schema独立于代码的数据模型描述语言Prisma Schema 在设计上最大程度贴近数据库结构描述同时对关联关系做了进一步抽象并在背后维护了与数据模型的对应关系。也就是说它几乎与数据库的定义一模一样唯一多出来的关系字段例如posts与author其实是在弥补数据库表关联外键中不直观的部分——Prisma 把这些外键转化为实体对象让操作时感受不到外键或者多表的存在在具体执行时再转化为 JOIN 操作。关于 JOIN 对列的拓展语义可对照本仓库 SQL 复杂查询 中的连接查询章节。下面是一份完整的 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声明链接数据库的信息。provider指定数据库类型如postgresql、mysql、sqlite、sqlserver等url通常通过env(DATABASE_URL)从环境变量读取连接串避免把敏感信息写死在 Schema 里。generator client声明使用 Prisma Client 进行客户端操作。provider prisma-client-js表示生成 JavaScript/TypeScript 客户端。从generator 是可配置的这一点可以推断Prisma Client 其实是可替换的实现——Prisma 允许通过不同 generator 生成不同语言的客户端。model最核心的模型定义。上面的示例中Post通过author字段与User建立 n:1 关联authorId外键引用User.id而User侧反向声明posts Post[]两边共同构成完整的双向关系描述。映射map 与 map在模型定义中可以通过map修改字段名映射、map修改表名映射。默认情况下字段名与 key 名相同。例如model Comment { title map(comment_title) map(comments) }上面这个模型在数据库中的表名是comments字段title对应的数据库列名是comment_title而 Prisma 代码层面依然使用Comment与title。这种解耦让代码命名与数据库列名可以各自独立演进。字段由四种描述组成Prisma 中每个字段由下面四种描述组成字段名。字段类型。可选的类型修饰如?表示可选、[]表示数组。可选的属性描述如id、default。model Tag { name String? id }在这个描述里包含字段名name、字段类型String、类型修饰?可空、属性描述id主键。字段类型模型类型、底层类型与 Unsupported字段类型可以是 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定义关联关系。上面的例子描述了Comment与Post存在 n:1 关系并且Comment.postId与Post.id关联Post侧用comments Comment[]表达一篇文章可以有多条评论Comment侧用Post Post?加relation(fields: [postId], references: [id])声明外键与引用目标。字段类型还可以是底层数据库数据类型通过db.描述。例如model Post { id db.TinyInt(1) }db.前缀用来直接指定数据库原生类型如TinyInt、VarChar(255)等实现比 Prisma 抽象类型更精确的底层控制。注意db.后缀在不同 providerPostgreSQL / MySQL / SQLite下可用的原生类型集合不同。对于 Prisma 不支持的类型还可以使用Unsupported修饰model Post { someField Unsupported(polygon)? }这种类型的字段无法通过 ORM API 查询但可以通过queryRaw方式查询。queryRaw是 ORM 对原始 SQL 模式的支持在 Prisma Client 部分会详细提到。类型修饰? 与 []类型修饰只有?与[]两种语法model User { name String? posts Post[] }?表示可选可空对应数据库中的 NULL 允许[]表示数组即一对多的多侧对应数据库中的关系集合。属性描述字段级与模型级属性描述是 Prisma Schema 中承载约束与语义的关键语法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()直接调用数据库底层的函数比如dbgenerated(gen_random_uuid())cuid()生成 cuid 字符串uuid()生成 uuid 字符串now()当前时间。unique设置字段值唯一对应数据库 UNIQUE 约束。relation设置关联上面已经提到。map设置字段名映射上面已经提到。updatedAt修饰字段用来存储上次更新时间一般是数据库自带的能力Prisma 在更新记录时自动维护该值。ignore对 Prisma 标记无效的字段即该字段不会进入数据库同步与查询。所有属性描述都可以组合使用。此外还存在模型model级别的描述一般用两个描述包括id复合主键如id([year, quarter])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]) }显式版本的CategoriesOnPosts就是那张第三张表除了两个外键还可以附带业务字段如assignedAt、assignedBy并使用id([postId, categoryId])声明复合主键。隐式写法由 Prisma 自动生成类似的关联表显式写法则允许开发者完全掌控关联表结构。对应背后生成的 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 隐式多对多的本质依然是关系表 外键 唯一索引只是把这段样板 SQL 藏了起来。Prisma Client类型安全的增删改查描述好 Prisma Model 后执行prisma generate再通过npm install prisma/client安装好 Node 包就可以在代码里操作 ORM 了import { PrismaClient } from prisma/client const prisma new PrismaClient()PrismaClient会根据 Schema 生成每个 model 的增删改查方法并且带完整的 TypeScript 类型提示传入错误的字段名或类型会在编译期直接报错。CRUD覆盖完整生命周期使用create创建一条记录const user await prisma.user.create({ data: { email: elsaprisma.io, name: Elsa Prisma, }, })使用createMany创建多条记录skipDuplicates跳过唯一键冲突的行const createMany await prisma.user.createMany({ data: [ { name: Bob, email: bobprisma.io }, { name: Bobo, email: bobprisma.io }, // Duplicate unique key! { name: Yewande, email: yewandeprisma.io }, { name: Angelique, email: angeliqueprisma.io }, ], skipDuplicates: true, // Skip Bobo })使用findUnique查找单条记录where需要命中唯一字段id或uniqueconst user await prisma.user.findUnique({ where: { email: elsaprisma.io, }, })对于联合索引/复合主键的情况比如下面的模型model TimePeriod { year Int quarter Int total Decimal id([year, quarter]) }findUnique需要再嵌套一层由_拼接的 keyconst timePeriod await prisma.timePeriod.findUnique({ where: { year_quarter: { quarter: 4, year: 2020, }, }, })复合键的名字由字段名按下划线连接生成yearquarter→year_quarter外层套一个对象传入两个字段的值。使用findMany查询多条记录const users await prisma.user.findMany()findMany可以使用 SQL 中各种条件语句语法如下const users await prisma.user.findMany({ where: { role: ADMIN, }, include: { posts: true, }, })使用update更新记录const updateUser await prisma.user.update({ where: { email: violaprisma.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: bertprisma.io, }, })使用deleteMany按条件批量删除const deleteUsers await prisma.user.deleteMany({ where: { email: { contains: prisma.io, }, }, })注意update与delete的where同样需要唯一字段非唯一条件只能走updateMany/deleteMany。include关联查询与嵌套展开使用include表示关联查询是否生效const getUser await prisma.user.findUnique({ where: { id: 19, }, include: { posts: true, }, })这样就会在查询user表时顺带查询所有关联的post表底层转化为一次包含 JOIN 的查询。关联查询也支持嵌套const user await prisma.user.findMany({ include: { posts: { include: { categories: true, }, }, }, })include的嵌套层级与 Schema 中声明的关联关系一一对应可以一层层展开到任意深度。筛选条件大全Prisma 的where筛选条件支持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 nameEleanor即通过对象嵌套的方式表达语义。把条件集合整理成表更直观条件语义对应 SQL 直觉equals相等not不相等!in属于集合IN (...)notIn不属于集合NOT IN (...)lt/lte小于 / 小于等于/gt/gte大于 / 大于等于/contains包含子串LIKE %x%search全文搜索视数据库而定mode大小写敏感模式如mode: insensitive视数据库而定startsWith/endsWith前缀 / 后缀匹配LIKE x%/LIKE %xAND/OR/NOT逻辑组合AND/OR/NOT条件对象可以在字段级别嵌套如上例name: { equals: ... }也可以在where顶层用AND: [...]、OR: [...]组合多个子条件。理解这些条件的语义时建议对照本仓库 SQL 入门 中WHERE 基于行筛选、HAVING 基于组合筛选的区分避免在聚合场景下用错条件层级。原生 SQL$queryRawPrisma 也可以直接写原生 SQL用于 ORM 不便表达或需要精细调优的场景const email emelieprisma.io const result await prisma.$queryRaw( Prisma.sqlSELECT * FROM User WHERE email ${email} )Prisma.sql标签模板会自动处理参数绑定避免字符串拼接带来的 SQL 注入风险。这也是前面Unsupported类型字段唯一可行的查询通道。中间件洋葱模型的执行拓展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可以看到中间件执行顺序是典型的洋葱模型最外层中间件最先拿到params执行前置逻辑调用next(params)进入下一层直到最内层真正执行数据库操作然后结果再逐层返回后置逻辑逆序执行。每个操作无论create还是其他 CRUD 方法都会触发全部中间件因此可以在next前后插入任意业务逻辑例如统一记录操作日志、打印参数可以对操作耗时进行打点记录监控慢查询也可以拦截或改写params实现权限校验、字段脱敏等横切关注点。在实际使用中这是规避 ORM 性能问题的关键抓手利用中间件监控查询性能对性能较差的地方改用prisma.$queryRaw原生 SQL 查询。精读ORM 设计模式与 Prisma 的取舍ORM 的两种设计模式ORM 有Active Record与Data Mapper两种设计模式Active Record对象背后完全对应 SQL 查询对象本身就承担知道自己如何持久化的职责。这种模式现在已经不怎么流行了Data Mapper对象并不知道数据库的存在中间多了一层映射甚至背后不需要对应数据库所以可以做一些很轻量的调试功能。Prisma 采用了 Data Mapper 模式你的业务代码操作的是纯对象Pure Object真正与数据库打交道的是 Prisma Client 内部的映射层。ORM 容易引发性能问题当数据量大或者性能、资源敏感的情况下我们需要对 SQL 进行优化甚至需要针对特定 MySQL 版本的某些内核错误对 SQL 进行看似无意义的申明调优比如在where之前再进行相同条件的 IN 范围限定有时能取得惊人的性能提升。而 ORM 建立在一个较为理想化的理论基础之上——即数据模型可以很好的转化为对象操作。问题在于对象操作屏蔽了细节我们无法对 SQL 进行针对性调优。另外得益于对象操作的便利性我们很容易通过obj.obj.的方式访问某些属性但这背后生成的却是一系列未经优化或仅部分自动优化的复杂 JOIN SQL。手写 SQL 时我们会提前考虑性能因素但通过对象调用时却因为调用成本低或觉得 ORM 有 magic 优化等想法写出很多实际上不合理的 SQL。对照 SQL 复杂查询 中JOIN 不仅拓展了列还会随之拓展行的说明可以更清楚地意识到嵌套include越深生成的 JOIN 越复杂行数膨胀与查询代价越不可控。Prisma Schema 的好处其实从语法上Prisma Schema 与 TypeORM 基于 Class 装饰器的拓展几乎可以等价转换但 Prisma Schema 在实际使用中有一个很不错的优势减少样板代码以及稳定数据库模型。减少样板代码比较好理解因为 Prisma Schema 并不会出现在业务代码中。而稳定模型是指只要不执行prisma generate数据模型就不会变化。而且 Prisma Schema 独立于 Node 存在甚至可以不放在项目源码中相比之下修改起来会更加慎重而完全用 Node 定义的模型因为本身是代码的一部分可能会被随手修改而且也没有执行数据库结构同步的操作。如果项目采用 Prisma模型变更后可以执行如下流程prisma db pull将数据库现有结构同步到 Schema或反向用prisma migrate把 Schema 变更应用到数据库prisma generate更新客户端 API。这个拉取/迁移 → 重新生成的闭环让数据库结构演进与客户端类型同步保持清晰、可追踪。API 设计对比与统一结构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: aliceprisma.io, }) // prisma const newUser await prisma.user.create({ data: { name: Alice, }, })Prisma 的优势在于首先存在prisma这个顶层变量使用起来非常方便其次从 API 拓展性上来说虽然 Mongoose 设计得更简洁但添加一些条件时拓展性会不足导致结构不太稳定不利于统一记忆。Prisma Client 的 API 统一采用下面这种结构await prisma.modelName.operateName({ // 数据比如 create、update 时会用到 data: /** ... */, // 条件大部分情况都可以用到 where: /** ... */, // 其它特殊参数或者 operater 特有的参数 })prisma.模型名.操作方法({ ... })的四段式签名顶层prisma→ 模型名 → 操作名 → 参数对象让所有 CRUD 调用保持一致的记忆模型学习成本被显著摊薄。总结与实战建议归纳一下Prisma Schema 是 Prisma 的一大特色因为这部分描述独立于代码带来了如下几个好处定义比 Node Class 更简洁无需装饰器与样板类纯声明式描述不生成冗余的代码结构模型定义与业务代码解耦Prisma Client 更加轻量且查询返回的都是 Pure Object便于调试与序列化。整体来看Prisma 虽然没有对 ORM 做出革命性改变但在微创新与 API 优化上都做得足够好如果你决定使用 ORM 开发项目Prisma 是比较推荐的选择。实战中规避性能问题的标准组合拳是用中间件统一打点通过prisma.$use记录每个操作的耗时与参数建立慢查询监控对热点查询降级为原生 SQL对性能较差的地方采用prisma.$queryRaw手写并调优 SQL兼顾 ORM 的开发效率与原生查询的调优能力谨慎控制include深度避免无意识的深层关联展开生成复杂的 JOIN必要时拆分为多次查询。延伸阅读可继续翻阅本仓库的 SQL 入门、SQL 聚合查询、SQL 复杂查询 与 SQL CASE 表达式它们从集合视角解释了 Prisma 查询条件背后的 SQL 语义两者对照阅读可以建立更完整的数据库操作认知。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Prisma ORM 实战指南用 Prisma Schema、Prisma Client 与 Prisma Migrate 告别手写 SQLPrisma ORM 实战指南用 Prisma Schema、Prisma Client 与 Prisma Migrate 告别手写 SQL 本指南基于 cu文档教程教育使用 Prisma 引导构建基于 Node.js 的 GraphQL 服务端从 graphql-yoga 到 prisma-binding 的完整实战使用 Prisma 引导构建基于 Node.js 的 GraphQL 服务端从 graphql yoga 到 prisma binding 的完整实战 本篇技后端数据库GraphQL使用 Prisma、React 与 Apollo Boost 从前端直接连接 Prisma 服务完整实战教程使用 Prisma、React 与 Apollo Boost 从前端直接连接 Prisma 服务完整实战教程 本教程基于 Prisma 开源仓库 prism后端数据库GraphQL创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网