使用Nest.js和GraphQL实现CRUD接口 -- 知识铺
我们经常用 restful 的接口来开发业务。
比如 GET 请求 /students 查询所有学生,/students/1 查询 id 为 1 的学生
发送 POST、PUT、DETETE 请求分别代表增删改。
其实也可以用 GraphQL 的方式来写接口:
查询:
新增:
增删改查都在一个接口里搞定,并且想要什么数据由前端自己取。
今天我们就用 Nest + GrahQL 做一个 TodoList 的增删改查。
数据存在 mysql 里,用 Prisma 作为 ORM 框架。
<span></span><code>npm install -g @nestjs/cli<br><br>nest new graphql-todolist<br></code>
创建个项目,然后我们首先来实现 restful 接口的增删改查。
用 docker 把 mysql 跑起来:
从 docker 官网下载 docker desktop,这个是 docker 的桌面端:
跑起来后,搜索 mysql 镜像(这步需要科学上网),点击 run:
输入容器名、端口映射、以及挂载的数据卷,还要指定一个环境变量:
端口映射就是把宿主机的 3306 端口映射到容器里的 3306 端口,这样就可以在宿主机访问了。
数据卷挂载就是把宿主机的某个目录映射到容器里的 /var/lib/mysql 目录,这样数据是保存在本地的,不会丢失。
而 MYSQL_ROOT_PASSWORD 的密码则是 mysql 连接时候的密码。
跑起来后,我们用 GUI 客户端连上,这里我们用的是 mysql workbench,这是 mysql 官方提供的免费客户端:
连接上之后,点击创建 database:
指定名字、字符集为 utf8mb4,然后点击右下角的 apply。
创建成功之后在左侧就可以看到这个 database 了:
现在还没有表。
我们在 Nest 里用 Prisma 连接 mysql。
进入项目,安装 prisma
<span></span><code>npm install prisma --save-dev<br></code>
执行 prisma init 创建 schema 文件:
<span></span><code>npx prisma init<br></code>
生成了 schema 文件(用来定义 model 的),和 .env 文件:
改下 .env 的配置:
<span></span><code>DATABASE_URL="mysql://root:你的密码@localhost:3306/todolist"<br></code>
并且修改下 schema 里的 datasource 部分:
<span></span><code>datasource db {<br> provider = <span>"mysql"</span><br> url = env(<span>"DATABASE_URL"</span>)<br>}<br></code>
然后创建 model:
<span></span><code>generator client {<br> provider = <span>"prisma-client-js"</span><br>}<br><br>datasource db {<br> provider = <span>"mysql"</span><br> url = env(<span>"DATABASE_URL"</span>)<br>}<br><br>model TodoItem {<br> id Int @id @<span>default</span>(autoincrement())<br> content <span>String</span> @db.VarChar(<span>50</span>)<br> createTime DateTime @<span>default</span>(now())<br> updateTime DateTime @updatedAt<br>}<br></code>
id 自增,content 是长度为 50 的字符串,还有创建时间 createTime、更新时间 updateTime。
执行 prisma migrate dev,它会根据定义的 model 去创建表:
<span></span><code>npx prisma migrate dev --name init<br></code>
它会生成 sql 文件,里面是这次执行的 sql。
然后还会生成 client 代码,用来连接数据库操作这个表。
可以看到,这次执行的 sql 就是 create table 建表语句:
这时候数据库就就有这个表了:
接下来我们就可以在代码里做 CRUD 了。
生成一个 service:
<span></span><code>nest g service prisma --flat --no-spec<br></code>
改下生成的 PrismaService,继承 PrismaClient,这样它就有 crud 的 api 了:
<span></span><code><span>import</span> { Injectable, OnModuleInit } <span>from</span> <span>'@nestjs/common'</span>;<br><span>import</span> { PrismaClient } <span>from</span> <span>'@prisma/client'</span>;<br><br>@Injectable()<br><span>export</span> <span><span>class</span> <span>PrismaService</span> <span>extends</span> <span>PrismaClient</span> <span>implements</span> <span>OnModuleInit</span> </span>{<br><br> <span>constructor</span>() {<br> <span>super</span>({<br> <span>log</span>: [<br> {<br> <span>emit</span>: <span>'stdout'</span>,<br> <span>level</span>: <span>'query'</span><br> }<br> ]<br> })<br> }<br><br> <span>async</span> onModuleInit() {<br> <span>await</span> <span>this</span>.$connect();<br> }<br>}<br></code>
在 constructor 里设置 PrismaClient 的 log 参数,也就是打印 sql 到控制台。
在 onModuleInit 的生命周期方法里调用 $connect 来连接数据库。
然后在 AppService 里注入 PrismaService,实现 CRUD:
<span></span><code><span>import</span> { Inject, Injectable } <span>from</span> <span>'@nestjs/common'</span>;<br><span>import</span> { PrismaService } <span>from</span> <span>'./prisma.service'</span>;<br><span>import</span> { CreateTodoList } <span>from</span> <span>'./todolist-create.dto'</span>;<br><span>import</span> { UpdateTodoList } <span>from</span> <span>'./todolist-update.dto'</span>;<br><br>@Injectable()<br><span>export</span> <span><span>class</span> <span>AppService</span> </span>{<br><br> getHello(): string {<br> <span>return</span> <span>'Hello World!'</span>;<br> }<br><br> @Inject(PrismaService)<br> private prismaService: PrismaService;<br><br> <span>async</span> query() {<br> <span>return</span> <span>this</span>.prismaService.todoItem.findMany({<br> <span>select</span>: {<br> <span>id</span>: <span>true</span>,<br> <span>content</span>: <span>true</span>,<br> <span>createTime</span>: <span>true</span><br> }<br> });<br> }<br><br> <span>async</span> create(todoItem: CreateTodoList) {<br> <span>return</span> <span>this</span>.prismaService.todoItem.create({<br> <span>data</span>: todoItem,<br> <span>select</span>: {<br> <span>id</span>: <span>true</span>,<br> <span>content</span>: <span>true</span>,<br> <span>createTime</span>: <span>true</span><br> }<br> });<br> }<br><br> <span>async</span> update(todoItem: UpdateTodoList) {<br> <span>return</span> <span>this</span>.prismaService.todoItem.update({<br> <span>where</span>: {<br> <span>id</span>: todoItem.id<br> },<br> <span>data</span>: todoItem,<br> <span>select</span>: {<br> <span>id</span>: <span>true</span>,<br> <span>content</span>: <span>true</span>,<br> <span>createTime</span>: <span>true</span><br> }<br> });<br> }<br><br> <span>async</span> remove(id: number) {<br> <span>return</span> <span>this</span>.prismaService.todoItem.delete({<br> <span>where</span>: {<br> id<br> }<br> })<br> }<br>}<br></code>
@Inject 注入 PrismaService,用它来做 CRUD,where 是条件、data 是数据,select 是回显的字段:
然后创建用到的两个 dto 的 class
todolist-create.dto.ts
<span></span><code><span>export</span> <span><span>class</span> <span>CreateTodoList</span> </span>{<br> <span>content</span>: string;<br>}<br></code>
todolist-update.dto.ts
<span></span><code><span>export</span> class UpdateTodoList {<br> id: number;<br> content: string;<br>}<br></code>
在 AppController 里引入下,添加几个路由:
<span></span><code><span>import</span> { Body, Controller, Delete, Get, Post, Query } <span>from</span> <span>'@nestjs/common'</span>;<br><span>import</span> { AppService } <span>from</span> <span>'./app.service'</span>;<br><span>import</span> { CreateTodoList } <span>from</span> <span>'./todolist-create.dto'</span>;<br><span>import</span> { UpdateTodoList } <span>from</span> <span>'./todolist-update.dto'</span>;<br><br>@Controller()<br><span>export</span> <span><span>class</span> <span>AppController</span> </span>{<br> <span>constructor</span>(private readonly appService: AppService) {}<br><br> @Get()<br> getHello(): string {<br> <span>return</span> <span>this</span>.appService.getHello();<br> }<br><br> @Post(<span>'create'</span>)<br> <span>async</span> create(@Body() todoItem: CreateTodoList) {<br> <span>return</span> <span>this</span>.appService.create(todoItem);<br> }<br><br> @Post(<span>'update'</span>)<br> <span>async</span> update(@Body() todoItem: UpdateTodoList) {<br> <span>return</span> <span>this</span>.appService.update(todoItem);<br> }<br><br> @Get(<span>'delete'</span>)<br> <span>async</span> <span>delete</span>(@Query(<span>'id'</span>) id: number) {<br> <span>return</span> <span>this</span>.appService.remove(+id);<br> }<br><br> @Get(<span>'list'</span>)<br> <span>async</span> list() {<br> <span>return</span> <span>this</span>.appService.query();<br> }<br><br>}<br></code>
添加增删改查 4 个路由,post 请求用 @Body() 注入请求体,@Query 拿路径中的参数:
把服务跑起来试一下:
<span></span><code>npm run start:dev<br></code>
首先是 list,现在没有数据:
然后添加一个:
服务端打印了 insert into 的 sql:
数据库也有了这条记录:
再加一个:
然后查一下:
接下来试下修改、删除:
再查一下:
没啥问题。
这样,todolist 的 restful 版接口就完成了。
接下来实现 graphql 版本:
安装用到的包:
<span></span><code>npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql<br></code>
然后在 AppModule 里引入下:
<span></span><code><span>import</span> { Module } <span>from</span> <span>'@nestjs/common'</span>;<br><span>import</span> { AppController } <span>from</span> <span>'./app.controller'</span>;<br><span>import</span> { AppService } <span>from</span> <span>'./app.service'</span>;<br><span>import</span> { PrismaService } <span>from</span> <span>'./prisma.service'</span>;<br><span>import</span> { GraphQLModule } <span>from</span> <span>'@nestjs/graphql'</span>;<br><span>import</span> { ApolloDriver } <span>from</span> <span>'@nestjs/apollo'</span>;<br><br>@Module({<br> <span>imports</span>: [<br> GraphQLModule.forRoot({<br> <span>driver</span>: ApolloDriver,<br> <span>typePaths</span>: [<span>'./**/*.graphql'</span>],<br> })<br> ],<br> <span>controllers</span>: [AppController],<br> <span>providers</span>: [AppService, PrismaService],<br>})<br><span>export</span> <span><span>class</span> <span>AppModule</span> </span>{}<br></code>
typePaths 就是 schema 文件的路径:
添加一个 todolist.graphql
<span></span><code>type TodoItem {<br> id: Int<br> content: String<br>}<br><br>input CreateTodoItemInput {<br> content: String<br>}<br><br>input UpdateTodoItemInput {<br> id: Int!<br> content: String<br>}<br><br>type Query {<br> todolist: [TodoItem]!<br> queryById(id: Int!): TodoItem<br>}<br><br><br>type Mutation {<br> createTodoItem(todoItem: CreateTodoItemInput!): TodoItem!<br> updateTodoItem(todoItem: UpdateTodoItemInput!): TodoItem!<br> removeTodoItem(id: Int!): Int<br>}<br></code>
语法比较容易看懂,就是定义数据的结构。
在 Query 下定义查询的接口,在 Mutation 下定义增删改的接口。
然后实现 resolver,也就是这些接口的实现:
<span></span><code>nest g resolver todolist --no-spec --flat<br></code>
<span></span><code><span>import</span> { Args, Mutation, Query, Resolver } <span>from</span> <span>'@nestjs/graphql'</span>;<br><span>import</span> { PrismaService } <span>from</span> <span>'./prisma.service'</span>;<br><span>import</span> { Inject } <span>from</span> <span>'@nestjs/common'</span>;<br><span>import</span> { CreateTodoList } <span>from</span> <span>'./todolist-create.dto'</span>;<br><span>import</span> { UpdateTodoList } <span>from</span> <span>'./todolist-update.dto'</span>;<br><br>@Resolver()<br><span>export</span> <span><span>class</span> <span>TodolistResolver</span> </span>{<br><br> @Inject(PrismaService)<br> private prismaService: PrismaService;<br><br> @Query(<span>"todolist"</span>)<br> <span>async</span> todolist() {<br> <span>return</span> <span>this</span>.prismaService.todoItem.findMany();<br> }<br><br> @Query(<span>"queryById"</span>)<br> <span>async</span> queryById(@Args(<span>'id'</span>) id) {<br> <span>return</span> <span>this</span>.prismaService.todoItem.findUnique({<br> <span>where</span>: {<br> id<br> }<br> })<br> }<br><br> @Mutation(<span>"createTodoItem"</span>)<br> <span>async</span> createTodoItem(@Args(<span>"todoItem"</span>) todoItem: CreateTodoList) {<br> <span>return</span> <span>this</span>.prismaService.todoItem.create({<br> <span>data</span>: todoItem,<br> <span>select</span>: {<br> <span>id</span>: <span>true</span>,<br> <span>content</span>: <span>true</span>,<br> <span>createTime</span>: <span>true</span><br> }<br> });<br> }<br><br><br> @Mutation(<span>"updateTodoItem"</span>)<br> <span>async</span> updateTodoItem(@Args(<span>'todoItem'</span>) todoItem: UpdateTodoList) {<br> <span>return</span> <span>this</span>.prismaService.todoItem.update({<br> <span>where</span>: {<br> <span>id</span>: todoItem.id<br> },<br> <span>data</span>: todoItem,<br> <span>select</span>: {<br> <span>id</span>: <span>true</span>,<br> <span>content</span>: <span>true</span>,<br> <span>createTime</span>: <span>true</span><br> }<br> });<br> }<br><br> @Mutation(<span>"removeTodoItem"</span>)<br> <span>async</span> removeTodoItem(@Args(<span>'id'</span>) id: number) {<br> <span>await</span> <span>this</span>.prismaService.todoItem.delete({<br> <span>where</span>: {<br> id<br> }<br> })<br> <span>return</span> id;<br> }<br>}<br></code>
用 @Resolver 声明 resolver,用 @Query 声明查询接口,@Mutation 声明增删改接口,@Args 取传入的参数。
具体增删改查的实现和之前一样。
浏览器访问 http://localhost:3000/graphql 就是 playground,可以在这里查询:
左边输入查询语法,右边是执行后返回的结果。
当然,对新手来说这个 playground 不够友好,没有提示。
我们换一个:
<span></span><code><span>import</span> { Module } <span>from</span> <span>'@nestjs/common'</span>;<br><span>import</span> { AppController } <span>from</span> <span>'./app.controller'</span>;<br><span>import</span> { AppService } <span>from</span> <span>'./app.service'</span>;<br><span>import</span> { PrismaService } <span>from</span> <span>'./prisma.service'</span>;<br><span>import</span> { GraphQLModule } <span>from</span> <span>'@nestjs/graphql'</span>;<br><span>import</span> { ApolloDriver } <span>from</span> <span>'@nestjs/apollo'</span>;<br><span>import</span> { TodolistResolver } <span>from</span> <span>'./todolist.resolver'</span>;<br><span>import</span> { ApolloServerPluginLandingPageLocalDefault } <span>from</span> <span>'@apollo/server/plugin/landingPage/default'</span>;<br><br>@Module({<br> <span>imports</span>: [<br> GraphQLModule.forRoot({<br> <span>driver</span>: ApolloDriver,<br> <span>typePaths</span>: [<span>'./**/*.graphql'</span>],<br> <span>playground</span>: <span>false</span>,<br> <span>plugins</span>: [ApolloServerPluginLandingPageLocalDefault()],<br> })<br> ],<br> <span>controllers</span>: [AppController],<br> <span>providers</span>: [AppService, PrismaService, TodolistResolver],<br>})<br><span>export</span> <span><span>class</span> <span>AppModule</span> </span>{}<br></code>
试一下新增:
查询:
修改:
单个查询:
删除:
查询:
基于 GraphQL 的增删改查都成功了!
然后在 react 项目里调用下。
<span></span><code>npx create-vite<br></code>
进入项目,安装 @apollo/client
<span></span><code>npm install<br><br>npm install @apollo/client<br></code>
改下 main.tsx
<span></span><code><span>import</span> * <span>as</span> ReactDOM <span>from</span> <span>'react-dom/client'</span>;<br><span>import</span> { ApolloClient, InMemoryCache, ApolloProvider } <span>from</span> <span>'@apollo/client'</span>;<br><span>import</span> App <span>from</span> <span>'./App'</span>;<br><br><span>const</span> client = <span>new</span> ApolloClient({<br> <span>uri</span>: <span>'http://localhost:3000/graphql'</span>,<br> <span>cache</span>: <span>new</span> InMemoryCache(),<br>});<br><br><span>const</span> root = ReactDOM.createRoot(<span>document</span>.getElementById(<span>'root'</span>)!);<br><br>root.render(<br> <span><span><<span>ApolloProvider</span> <span>client</span>=<span>{client}</span>></span><br> <span><<span>App</span> /></span><br> <span></<span>ApolloProvider</span>></span></span>,<br>);<br><br></code>
创建 ApolloClient 并设置到 ApolloProvider。
然后在 App.tsx 里用 useQuery 发请求:
<span></span><code><span>import</span> { gql, useQuery } <span>from</span> <span>'@apollo/client'</span>;<br><br><span>const</span> getTodoList = gql<span>`<br> query Query {<br> todolist {<br> content<br> id<br> }<br> }<br>`</span>;<br><br>type TodoItem = {<br> <span>id</span>: number;<br> content: string;<br>}<br><br>type TodoList = {<br> <span>todolist</span>: <span>Array</span><TodoItem>;<br>}<br><br><span>export</span> <span>default</span> <span><span>function</span> <span>App</span>(<span></span>) </span>{<br> <span>const</span> { loading, error, data } = useQuery<TodoList>(getTodoList);<br><br> <span>if</span> (loading) <span>return</span> <span>'Loading...'</span>;<br> <span>if</span> (error) <span>return</span> <span>`Error! <span>${error.message}</span>`</span>;<br><br> <span>return</span> (<br> <span><span><<span>ul</span>></span><br> {<br> data?.todolist?.map(item => {<br> return <span><<span>li</span> <span>key</span>=<span>{item.id}</span>></span>{item.content}<span></<span>li</span>></span><br> })<br> }<br> <span></<span>ul</span>></span></span><br> );<br>}<br></code>
把服务跑起来:
<span></span><code>npm run dev<br></code>
这里涉及到的跨域,现在后端服务里开启下跨域支持:
可以看到,返回了查询结果:
然后加一下新增:
用 useMutation 的 hook,指定 refetchQueries 也就是修改完之后重新获取数据。
调用的时候传入 content 数据。
<span></span><code><span>import</span> { gql, useMutation, useQuery } <span>from</span> <span>'@apollo/client'</span>;<br><br><span>const</span> getTodoList = gql<span>`<br> query Query {<br> todolist {<br> content<br> id<br> }<br> }<br>`</span>;<br><br><span>const</span> createTodoItem = gql<span>`<br> mutation Mutation($todoItem: CreateTodoItemInput!) {<br> createTodoItem(todoItem: $todoItem) {<br> id<br> content<br> }<br> }<br>`</span>;<br><br>type TodoItem = {<br> <span>id</span>: number;<br> content: string;<br>}<br><br>type TodoList = {<br> <span>todolist</span>: <span>Array</span><TodoItem>;<br>}<br><br><span>export</span> <span>default</span> <span><span>function</span> <span>App</span>(<span></span>) </span>{<br> <span>const</span> { loading, error, data } = useQuery<TodoList>(getTodoList);<br><br> <span>const</span> [createTodo] = useMutation(createTodoItem, {<br> <span>refetchQueries</span>: [getTodoList]<br> });<br><br> <span>async</span> <span><span>function</span> <span>onClick</span>(<span></span>) </span>{<br> <span>await</span> createTodo({<br> <span>variables</span>: {<br> <span>todoItem</span>: {<br> <span>content</span>: <span>Math</span>.random().toString().slice(<span>2</span>, <span>10</span>)<br> }<br> }<br> })<br> }<br><br> <span>if</span> (loading) <span>return</span> <span>'Loading...'</span>;<br> <span>if</span> (error) <span>return</span> <span>`Error! <span>${error.message}</span>`</span>;<br><br> <span>return</span> (<br> <span><span><<span>div</span>></span><br> <span><<span>button</span> <span>onClick</span>=<span>{onClick}</span>></span>新增<span></<span>button</span>></span><br> <span><<span>ul</span>></span><br> {<br> data?.todolist?.map(item => {<br> return <span><<span>li</span> <span>key</span>=<span>{item.id}</span>></span>{item.content}<span></<span>li</span>></span><br> })<br> }<br> <span></<span>ul</span>></span><br> <span></<span>div</span>></span></span><br> );<br>}<br></code>
测试下:
数据库里也可能看到新增的数据:
这样,我们就能在 react 项目里用 graphql 做 CRUD 了。
案例代码上传了 github。
后端代码: https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/graphql-todolist
前端代码:https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/graphql-todolist-client
总结
我们实现了Restful 和GraphQL 版的 CRUD。
前端用 React + @apollo/client。
后端用 Nest + GraphQL + Prisma + MySQL。
GraphQL 主要是定义 schema 和 resolver 两部分,schema 是 Query、Mutation 的结构,resolver 是它的实现。
可以在 playground 里调用接口,也可以在 react 里用 @appolo/client 调用。
相比 restful 的版本,graphql 只需要一个接口,然后用查询语言来查,需要什么数据取什么数据,更加灵活。
业务开发中,你会选择用 GraphQL 开发接口么?
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek001/post/20240424/%E4%BD%BF%E7%94%A8Nest.js%E5%92%8CGraphQL%E5%AE%9E%E7%8E%B0CRUD%E6%8E%A5%E5%8F%A3--%E7%9F%A5%E8%AF%86%E9%93%BA/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com