新闻详情

新闻详情

首页 / 资讯中心 / 详情

Redwood 保存数据实战:从 Prisma 模型到 Contact 表单的完整链路

发布时间:2026/9/25 7:15:24来源:尧图网络
Redwood 保存数据实战:从 Prisma 模型到 Contact 表单的完整链路
后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载本篇指南以 Redwood 教程第三章保存数据Saving Data为骨架完整演示一条真实的数据链路如何在api/db/schema.prisma中定义Contact模型、通过 CLI 生成 SDL 与 Service、在前端页面中用useMutation提交 GraphQL mutation最后用 Service Validations、Toast 通知与表单重置把表单打磨成可上线的产品级体验。读完本文你将掌握 Redwood 中数据库模型 → GraphQL 接口 → 前端表单的全流程写法并理解requireAuth/skipAuth指令与secure by default背后的设计逻辑。一、在数据库中新增 Contact 模型Redwood 应用的数据层以 Prisma 为核心。打开api/db/schema.prisma在已有的Post模型之后追加一个Contact模型datasource db { provider sqlite url env(DATABASE_URL) } generator client { provider prisma-client-js binaryTargets native } model Post { id Int id default(autoincrement()) title String body String createdAt DateTime default(now()) } model Contact { id Int id default(autoincrement()) name String email String message String createdAt DateTime default(now()) }几个关键点id default(autoincrement())声明自增主键default(now())让数据库在插入时自动填充创建时间可选字段语法Prisma 用问号后缀表示可空例如name String?表示name可以是String或NULL这里所有字段都是必填的无?对应到 GraphQL 层就是带!的必填类型。模型改好后生成并应用数据库迁移yarn rw prisma migrate dev迁移命令会引导你为迁移命名这里可以命名为create contact。migrate dev会同步 Prisma Client让代码中能立刻使用db.contact。二、用 generate 命令创建 SDL 与 ServiceRedwood 约定 GraphQL schema 定义文件SDL与业务逻辑Service一一对应SDL 中的 Query/Mutation 字段会自动映射到 Service 中同名函数。运行yarn rw g sdl Contact该命令会在api目录下生成两类文件api/src/graphql/contacts.sdl.{js,ts}用 GraphQL SDL 语法定义 schemaapi/src/services/contacts/contacts.{js,ts}业务逻辑层同时生成配套测试文件。关于这个命令的实现细节可以查看仓库中的生成模板 sdl.ts.template模板会自动读取 Prisma 模型定义把每个字段映射为对应的 GraphQL 类型并为每个 Query/Mutation 自动附加requireAuth指令。打开生成的 SDL 文件你会看到与 Post scaffold 相同的模式export const schema gql type Contact { id: Int! name: String! email: String! message: String! createdAt: DateTime! } type Query { contacts: [Contact!]! requireAuth contact(id: Int!): Contact requireAuth } input CreateContactInput { name: String! email: String! message: String! } input UpdateContactInput { name: String email: String message: String } type Mutation { createContact(input: CreateContactInput!): Contact! requireAuth updateContact(id: Int!, input: UpdateContactInput!): Contact! requireAuth deleteContact(id: Int!): Contact! requireAuth } 1. Input Type 的设计哲学CreateContactInput与UpdateContactInput遵循 GraphQL 官方推荐的 Input Types 模式mutation 不直接罗列每个可写字段而是接收一个类型化对象。CreateContactInput中schema.prisma里必填的字段name、email、message全部标记为必填!因为缺少它们无法创建合法记录UpdateContactInput中所有字段都是可选的——你可能只想更新一个字段也可能更新全部字段。与其为每一种字段组合分别创建 Input 类型Redwood 选择了仅一个 Update 输入类型作为开发体验的最佳折中。另外Redwood 默认假设你的代码不会手动设置id或createdAt因此这两个字段被排除在 Input 类型之外如果你的数据库允许手动写入这些字段可以自行把它们加回CreateContactInput或UpdateContactInput。2. 必填语义的两种语法Prisma 中可选字段需要额外的?name String?GraphQL SDL 中必填字段需要额外的!name: String!。两种语法方向相反容易混淆写 schema 时务必留意。3. 指令Directivesecure by defaultSDL 中Query/Mutation后的requireAuth是一个 schema directive可在仓库中查看 skipAuth.ts 与redwoodjs/graphql-server包中的createValidatorDirective实现。它的含义是访问该 GraphQL 查询必须通过身份认证。关键事实在未配置认证体系时requireAuth恒返回true即任何匿名请求都能通过——因为登录这个概念此时还不存在一旦接入认证如 dbAuth、Auth0该指令就会执行真实的登录校验逻辑每个 Query/Mutation 至少要有一个 schema directive否则 Redwood 会直接报错。这是secure by default的核心宁可抛错也不要在你什么都没做的情况下把用户数据暴露到公网。因为联系人查询要返回全部留言、仅供站点主人查看保留默认的requireAuth恰好符合需求而createContact需要允许任何人匿名提交留言所以要把它的指令改成skipAuth同时删掉updateContact与deleteContact我们不打算让用户修改或删除留言。修改后的 SDLexport const schema gql type Contact { id: Int! name: String! email: String! message: String! createdAt: DateTime! } type Query { contacts: [Contact!]! requireAuth contact(id: Int!): Contact requireAuth } input CreateContactInput { name: String! email: String! message: String! } type Mutation { createContact(input: CreateContactInput!): Contact! skipAuth } 小技巧如果你只需要一个只读的 SDL不需要 create/update/delete可以给生成命令加--no-crud标志yarn rw g sdl Contact --no-crud这样只会生成一个返回全部记录的contacts查询。4. Service一行代码的 resolverRedwood 的约定式解析器让 Service 极其简洁。看生成的 contacts.ts该 fixture 与教程产物一致import type { QueryResolvers, MutationResolvers } from types/graphql import { db } from src/lib/db export const contacts: QueryResolvers[contacts] () { return db.contact.findMany() } export const contact: QueryResolvers[contact] ({ id }) { return db.contact.findUnique({ where: { id }, }) } export const createContact: MutationResolvers[createContact] ({ input }) { return db.contact.create({ data: input, }) }createContact()接收input参数后原样交给 Prisma 的create()。TypeScript 版本还会借助 Redwood 自动生成的QueryResolvers/MutationResolvers类型做强类型校验。SDL 文件删除的字段对应 Service 函数即使留着也无法被客户端调用但为保持整洁可以一并删除updateContact与deleteContact。三、用 GraphQL Playground 调试 API在深入前端实现之前先用 Playground 验证 API 层。yarn rw dev或yarn rw dev启动时会同时运行 API 与 Web 两个进程。打开新浏览器标签页访问http://localhost:8911/graphql这是 GraphQL Yoga 内置的 GraphiQL 界面一个 Web 版 GraphQL GUI。点击左上角 Docs 标签并展开query: Query可以看到由 SDL 文件定义的完整 schemaPlayground 会基于这些定义给出自动补全提示。例如执行以下查询获取数据库中所有帖子的 idquery { posts { id } }点击 Play 按钮执行。当某个 query/mutation 行为不符合预期时Playground 是定位问题是 API 层拼写错误还是 Web 层拼写错误的首选工具。四、前端用 useMutation 保存联系人后端 mutation 已就绪接下来在ContactPage中调用它。首先在组件文件顶部定义 mutation 常量放在 import 语句之后、组件之外import { Metadata, useMutation } from redwoodjs/web const CREATE_CONTACT gql mutation CreateContactMutation($input: CreateContactInput!) { createContact(input: $input) { id } } 这段 mutation 引用了 Contact SDL 中定义的createContact通过$input变量接收一个符合CreateContactInput结构{ name, email, message }的对象。接着调用 Redwood 的useMutationhook注意导入const [create] useMutationCreateContactMutation, CreateContactMutationVariables(CREATE_CONTACT)TypeScript 用户需要额外注意只要 dev server 在运行或执行yarn rw generate typesRedwood 就会根据你定义的CreateContactMutation自动生成同名 TypeScript 类型可以直接从types/graphql导入详见 Generated Types。create是执行 mutation 的函数接收一个带variables键的对象create({ variables: { input: { name: Rob, email: robredwoodjs.com, message: I love Redwood!, }, }, })由于Form提交时会把所有字段按 name 组装成一个对象onSubmit收到的data恰好就是input需要的格式所以可以直接透传const onSubmit: SubmitHandlerFormValues (data) { create({ variables: { input: data } }) }至此填写并提交表单即可在数据库中创建一条 Contact 记录可通过 Prisma Studio 或 GraphQL Playground 验证。五、打磨表单体验上禁用按钮与成功通知当前表单有三个体验问题重复点击会重复提交、用户不知道是否成功、服务端出错时无法提示。逐一解决。1. 提交期间禁用 Save 按钮useMutation返回的第二个数组元素是一组状态其中loading表示请求是否仍在进行、error携带失败信息const [create, { loading, error }] useMutationCreateContactMutation, CreateContactMutationVariables(CREATE_CONTACT)把 Save 按钮的disabled属性绑定到loadingSubmit disabled{loading}Save/Submit开发环境下提交太快可能看不出差别可以用 Chrome DevTools 的 Network 面板开启网络限速throttling模拟慢网就能看到按钮在等待响应期间变为禁用状态从而避免重复提交。2. 用 Toast 提示提交成功Redwood 内置了 react-hot-toast 用于快速弹出通知。useMutation接受一个选项对象作为第二个参数其中的onCompleted回调会在 mutation 成功完成后触发。在回调里调用toast()并在return中、Form之前放置Toaster组件import { Metadata, useMutation } from redwoodjs/web import { toast, Toaster } from redwoodjs/web/toast const ContactPage () { const [create, { loading, error }] useMutation CreateContactMutation, CreateContactMutationVariables (CREATE_CONTACT, { onCompleted: () { toast.success(Thank you for your submission!) }, }) // ... return ( Metadata titleContact descriptionContact page / Toaster / Form onSubmit{onSubmit} config{{ mode: onBlur }} error{error} {/* 字段省略 */} Submit disabled{loading}Save/Submit /Form / ) }Toast 的完整用法可以参考 toast-notifications 文档。六、打磨表单体验下服务端校验与错误展示客户端校验不够——永远不要信任浏览器发来的数据。需要在 API 侧补上同样的 email 格式校验。1. 为什么需要 Service 层校验GraphQL 的String!声明会自动拒绝为 null 的字段这是 API 层的第一道防线数据库的非空约束也会拦截 null这是第二道防线。但两者都有盲区当你在另一个 Service 内部调用该 Service 函数时请求根本不经过 GraphQL类型声明与格式校验全部失效。因此 Redwood 把业务逻辑收敛在 Service 层并提供了内置的 Service Validations见 services.md#service-validations。2. 使用 validate 做 email 校验在contactsService 中引入validate并校验email字段import { validate } from redwoodjs/api export const createContact: MutationResolvers[createContact] ({ input }) { validate(input.email, email, { email: true }) return db.contact.create({ data: input }) }三个参数逐一拆解第一个参数要校验的值这里取input中的email第二个参数TextField的name属性用于把错误定位回页面上的具体输入框第三个参数校验指令对象email: true表示启用内置的 email 格式校验器。validate的实现位于 validations.ts它遍历指令对象中的每个 validator逐一调用对应的校验函数任何失败都会抛出校验错误使createContact在写库之前中断。为了验证服务端校验确实生效可临时移除TextField上的 emailpattern校验让坏数据能到达服务端TextField nameemail validation{{ required: true, - pattern: { - value: /^[^][^.]\..$/, - message: Please enter a valid email address, - }, }} errorClassNameerror /3. FormError一处展示所有服务端错误Form还留了一手给Form传入error来自useMutation建立上下文并在表单顶部渲染FormError别忘了 importForm onSubmit{onSubmit} config{{ mode: onBlur }} error{error} FormError error{error} wrapperClassNameform-error / {/* 字段省略 */} /Form再次提交非法 email 时页面顶部会出现一段平实的错误摘要同时出错字段会被高亮——和客户端内联校验的表现一致。对多屏长表单尤其有用用户不必在一长串输入框里翻找红色报错顶部就能看到全部问题汇总。如果不需要摘要框直接移除FormError字段高亮仍然保留。FormError提供多组样式属性分别作用于消息的不同部分wrapperStyle/wrapperClassName整条消息的容器titleStyle/titleClassName标题如 Errors prevented this form...listStyle/listClassName存放错误列表的ullistItemStyle/listItemClassName每条错误的li。4. Service Validations 的更多能力validate()支持组合多条指令。例如一个高价二手车销售场景可以在createCar里一次性做多维度约束export const createCar ({ input }: Car) { validate(input.make, make, { inclusion: [Audi, BMW, Ferrari, Lexus, Tesla], }) validate(input.color, color, { exclusion: { in: [Beige, Mauve], message: No one wants that color } }) validate(input.hasDamage, hasDamage, { absence: true }) validate(input.vin, vin, { format: /[A-Z0-9]/, length: { equal: 17 } }) validate(input.odometer, odometer, { numericality: { positive: true, lessThanOrEqual: 10000 } }) return db.car.create({ data: input }) }内置指令涵盖inclusion/exclusion白/黑名单、absence必须为空、format正则格式、length长度、numericality数值范围等常用场景。自定义校验逻辑也能享受同样的错误处理机制——用validateWith包裹一个会抛错的函数抛出的错误会被转换为ServiceValidationError并携带原始消息返回客户端见 validations.tsvalidateWith(() { const oneWeekAgo new Date() oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) if (input.lastCarWashDate oneWeekAgo) { throw new Error(We dont accept dirty cars) } })为什么不直接写自定义函数因为 GraphQL 会吞掉普通错误、只回一句 Something went wrongvalidateWith能把你的错误消息原样送回客户端。七、提交成功后重置表单没有跳转的提交场景应当清空表单字段。这需要访问 React Hook Form 的reset()函数——而基本用法下Form内部悄悄调用了useForm()你接触不到它。解法是自己调用useForm()并把返回对象通过formMethods属性传回Form保证校验等功能继续生效import { FieldError, Form, FormError, Label, Submit, TextAreaField, TextField, useForm, } from redwoodjs/forms const ContactPage () { const formMethods useForm() const [create, { loading, error }] useMutation CreateContactMutation, CreateContactMutationVariables (CREATE_CONTACT, { onCompleted: () { toast.success(Thank you for your submission!) formMethods.reset() }, }) return ( Toaster / Form onSubmit{onSubmit} config{{ mode: onBlur }} error{error} formMethods{formMethods} FormError error{error} wrapperClassNameform-error / {/* 字段省略 */} Submit disabled{loading}Save/Submit /Form / ) }一个容易踩的坑一旦改为自己调用useForm()onBlur等配置就不会再被自动应用——因为此前是 Redwood 替你调用useForm()并自动传入Form的configprop。现在需要手动传参const formMethods useForm({ mode: onBlur })React Hook Form 提供了大量Form未暴露的功能当你需要它们时就照此模式调用useForm()并把结果传回Form。八、完整页面代码把以上所有改进合并后ContactPage 的最终形态如下import { Metadata, useMutation } from redwoodjs/web import { toast, Toaster } from redwoodjs/web/toast import { FieldError, Form, FormError, Label, Submit, SubmitHandler, TextAreaField, TextField, useForm, } from redwoodjs/forms import { CreateContactMutation, CreateContactMutationVariables, } from types/graphql const CREATE_CONTACT gql mutation CreateContactMutation($input: CreateContactInput!) { createContact(input: $input) { id } } interface FormValues { name: string email: string message: string } const ContactPage () { const formMethods useForm({ mode: onBlur }) const [create, { loading, error }] useMutation CreateContactMutation, CreateContactMutationVariables (CREATE_CONTACT, { onCompleted: () { toast.success(Thank you for your submission!) formMethods.reset() }, }) const onSubmit: SubmitHandlerFormValues (data) { create({ variables: { input: data } }) } return ( Metadata titleContact descriptionContact page / Toaster / Form onSubmit{onSubmit} config{{ mode: onBlur }} error{error} formMethods{formMethods} FormError error{error} wrapperClassNameform-error / Label namename errorClassNameerror Name /Label TextField namename validation{{ required: true }} errorClassNameerror / FieldError namename classNameerror / Label nameemail errorClassNameerror Email /Label TextField nameemail validation{{ required: true, pattern: { value: /^[^][^.]\..$/, message: Please enter a valid email address, }, }} errorClassNameerror / FieldError nameemail classNameerror / Label namemessage errorClassNameerror Message /Label TextAreaField namemessage validation{{ required: true }} errorClassNameerror / FieldError namemessage classNameerror / Submit disabled{loading}Save/Submit /Form / ) } export default ContactPage提醒email 的客户端pattern校验可以放回TextField以提供即时反馈但服务端校验务必保留作为最后的防线。小结本文沿 Redwood 教程第三章的路径走完了保存数据的完整链路数据模型在api/db/schema.prisma定义Contact用yarn rw prisma migrate dev迁移API 层yarn rw g sdl Contact生成 SDL 与 Service理解 Input Type 设计、!/?必填语义差异以及requireAuth/skipAuth指令背后的 secure by default 原则调试通过http://localhost:8911/graphql的 GraphQL Playground 直接验证 API前端useMutation gql 常量完成提交loading禁用按钮、onCompletedToaster发成功通知、Service 层validateFormError展示服务端错误、useForm()formMethods实现提交后重置。这四条链路共同构成了 Redwood 全栈开发中最常见的表单写库模式。下一步自然是把这些管理功能收进 admin 区域并加上登录保护——那正是认证章节要解决的事。相关延伸阅读tutorial 第二章Redwood 如何处理数据、Prisma Studio 验证数据、Service Validations 完整参考、Toast 通知。赞分享后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载相关推荐Redwood 教程保存数据——从 Contact 模型到表单提交的完整数据写入实战Redwood 教程保存数据——从 Contact 模型到表单提交的完整数据写入实战 本篇教程带你走完 Redwood 中保存数据的完整闭环从在 sch后端前端Web框架开发工具Redwood 保存数据实战从 Prisma 模型到联系表单的全栈闭环Redwood 保存数据实战从 Prisma 模型到联系表单的全栈闭环 本指南是 Redwood 教程保存数据章节的完整实战讲解以博客的联系我们表单后端前端Web框架开发工具RedwoodJS 教程从 Prisma 数据模型到前端表单构建完整的数据保存链路RedwoodJS 教程从 Prisma 数据模型到前端表单构建完整的数据保存链路 本篇教程来自 RedwoodJS v1.x 官方教程第三章 savin后端前端Web框架开发工具上一篇终极解决方案用DxWrapper轻松修复Windows 10/11上的老游戏兼容性问题下一篇ModularizationExample性能优化模块化架构如何提升Android应用构建速度创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

如何5分钟快速上手dsh-anchored-standard:面向初学者的完整安装教程 2026/9/25 7:42:40

如何5分钟快速上手dsh-anchored-standard:面向初学者的完整安装教程

如何5分钟快速上手dsh-anchored-standard:面向初学者的完整安装教程 【免费下载链接】dsh-anchored-standard Two-phase DeepSeek Harness preset: Minimal-aligned bootstrap, then full Standard tools (Project2 98/99) 项目地址: https://gitcode.com/gh_mirr…

阅读更多 →
ZYNQ与AD9361软件无线电开发实战:初始化调试与工程落地避坑指南 2026/9/25 7:42:33

ZYNQ与AD9361软件无线电开发实战:初始化调试与工程落地避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
lego 使用 Ionos Cloud DNS 提供者签发 Let‘s Encrypt 证书:完整配置与原理剖析 2026/9/25 7:41:55

lego 使用 Ionos Cloud DNS 提供者签发 Let‘s Encrypt 证书:完整配置与原理剖析

网络安全密码学 【免费下载链接】lego Lets Encrypt/ACME client and library written in Go 项目地址: https://gitcode.com/gh_mirrors/le/lego 点击查看 免费下载 本文是 lego(Lets Encrypt/ACME client,使用 Go 编写)官方文档…

阅读更多 →
大疆OcuSync图传技术解析:从协议到实飞调参指南 2026/9/25 7:41:48

大疆OcuSync图传技术解析:从协议到实飞调参指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
ESP32 如何运行 WebAssembly?深入解析 Runtime 机制与 WAMR 实践 2026/9/25 7:41:48

ESP32 如何运行 WebAssembly?深入解析 Runtime 机制与 WAMR 实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Mac打印机连接故障排查与CUPS底层原理详解 2026/9/25 7:41:48

Mac打印机连接故障排查与CUPS底层原理详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉