阅读 3105

下一代前端全栈框架核心——Prisma(附源码实践)

作者:王洁

BLITZ.JS 与REDWOOD.JS

随着跨端技术的发展、TS的兴起、5G时代的来临,这些都推动着前端领域向大前端前进,前端在“技术层面“也渐渐发展到了稳定期,所以前端 -> api -> 后端 -> 数据库,还有这条链路上的类型安全,就成了一个前端的发展方向。

而Blitz.js 以及 Redwood.js都在向着这个发展方向进行靠拢。

blitz.js

我们都知道Next.js主要是一个前端框架,它是为构建连接到其它 API 的前端而设计的。Next.js 并不是一个真正的、开箱即用的全栈框架。而Blitz 是一个前端基于React(Next.js框架),后端基于Prisma,并且没有中间件GraphQL的全栈框架,它添加了Next.js所有缺少的功能,并且它具有“零 API”数据层抽象,当需要提供 API 给更多端使用时,再结合相关库来生成 API(useQuery、 useMutation函数,作为前后端的桥梁)。但是当然,我们依然可以像往常一样继续通过 REST 或 GraphQL 获取数据,blitz.js不会以任何方式限制这一点。这些功能使 Blitz 变成了真正的全栈框架。这些关键功能包括直接访问数据库,中间件和身份验证。在 Blitz.js 来在编译构建时,会把属于后端的代码抽到后端,然后在开发时,就会把整个前后端视为一个整体。

redwood.js

这是一个基于Jamstack的全栈框架,它的前端使用react,后端使用prisma2,中间是apollo graphql,并且它前端包装了一个cell组件的概念,这是一种更简单的异步数据获取方式。Cell 是一个高阶组件,包含 graphql 查询、加载、空、错误或成功等状态。这些状态中的每一个都可以使自动感知cell所处的状态。它有内置特色 Redwood Router为 Redwood 应用程序提供强大而简单的路由。这是一个比 React Router 更好的解决方案——比 React Router 更直接、更容易使用。

以上的两个新型全栈框架都是基于prisma作为后端技术,两者框架的优劣或者想对这两个框架进行深入了解,可在官网进行查阅,它们不作为本篇文章的重点。本篇文章我们来着重的讲一下:

为什么下一代的新型全栈框架会选择prisma作为后端技术,它作为一个新型ORM具有哪些优势?

ORM前言介绍

我们知道数据库的交互方式主要有三种,分别是ORM、SQL构造器(SQL Query Builder)以及原生SQl。

  • 原生SQL:使用原生SQL可以完全控制数据库操作,但是由于向数据库发送纯 SQL 字符串很麻烦,并且会带来大量开销,所以生产力很低。

  • SQL Query Builder:这是一种半自动的SQL框架(例如knex.js),构造器可能会出现存在多个方法的链式调用,这其实是更贴近SQL语句,通过工具封装的API来生成SQL。构造器的优点就是控制力强,但是缺点就是应用程序开发人员仍然需要根据 SQL 来考虑他们的数据,容易导致数据在认知层面与实际层面存在差异。

  • ORM:ORM的全称是Object-Relationl Mapping,这是在对象与数据库之间做了一个映射,可以说是一种全自动的SQl框架,这样在具体操作数据库时,操作对象就是操作表里的一条记录,表中的字段对应对象的属性。使用者不需要使用复杂的SQL语句,只需要像平常操作对象那样去操作它,ORM就会生成可执行的SQL来进行数据库的操作。ORM的优点就是生产力强,但是一旦碰到复杂的SQl效率就会极低。

三者代码简单对比: 1631784201255.jpg

三者生产力和控制力对比:

ORM、SQL 查询构建器和 SQL 中的生产力与控制

与TYPEORM对比

ORM模式

TypeORM是TypeScript生态系统中一个传统流行的 ORM,如果我们打开TypeORM,官方文档中提到:

同时支持 DataMapper 和 ActiveRecord

这两种模式在ORM是常见的,它们在对象与数据库之间传输数据的方式上会有不同:

  • ActiveRecord:它将模型类映射到数据库表,每个字段在数据库表中都有一个匹配的列。简单来说,它是一种在模型中访问数据库的方法。
  • DataMapper:它是一个对象和数据库记录的中间层(映射器类),旨在使二者隔离,同时和ActiveRecord一样提供数据的双向传递。这意味着使用 Data Mapper 时,内存中的对象(代表数据库中的数据)甚至不知道存在数据库。

但TypeORM的DataMapper并不是严格遵循Data Mapper ORM这样的模式。它采取这样的方法:

  • 实体类使用装饰器 (@Column) 将类属性映射到表列并感应数据库的变化。

  • 它用存储库类来代替了映射器类,存储库类用于查询数据库,并且可能包含自定义查询。存储库使用装饰器来确定实体属性和数据库列之间的映射。

    import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
    
    @Entity()
    export class User {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column({ name: 'first_name' })
      firstName: string
      
      @Column({ name: 'last_name' })
      lastName: string
    }
    
    const userRepository = connection.getRepository(User);
    const user = new User();
    复制代码

TypeORM他对ActiveRecord采取这样的方法:

  • 实体类使用装饰器 (@Column) 将类属性映射到表列并感应数据库的变化。

  • 需要让实体类继承BaseEntity类,这样实体类上就具有了各种方法。

    import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm";
    ​
    @Entity()
    export class User extends BaseEntity {
      // 所有 active-record 实体都必须扩展BaseEntity类,它提供了与实体一起使用的方法。
      @PrimaryGeneratedColumn()
      id: number;
    ​
      @Column({ name: 'first_name' })
      firstName: string
    ​
      @Column({ name: 'last_name' })
      lastName: string
    }
    ​
    const user = new User();
    复制代码

对于Active Record ORM模式,TypeORM 的方法通常会导致复杂的模型实例随着应用程序的增长而变得难以维护。

而prisma则遵循 DataMapper ORM 模式而不是 Active Record。Prisma如何实现DataMapper,做了简要对比:

概念描述传统ORMPrismaPrisma真实的数据来源
对象模型应用程序中的内存数据结构Model classes模型类生成的TypeScript types类型Prisma模式中的模型
Data Mapper在对象模式和数据库之间转换的代码Mapper classes映射器类由PrismaClient生成的函数Prisma模式中的@map属性
数据库架构数据库中数据的结构,如表和列手动或编程API编写的SQL由Prisma Migrate生成的SQLPrisma模式

Prisma遵循DataMapper模式并附带了一些优势:比如它可以生成基于Prisma模式的Prisma Client,从而减少减少定义类和映射逻辑的样板文件以及消除程序对象和数据库模式之间的同步问题等。

迁移

同时,因为 ORM 位于开发人员和数据库之间,所以大多数 ORM 都提供了一个迁移工具来协助创建和修改数据库模式。迁移是将数据库模式从一种状态转移到另一种状态的一组步骤。第一次迁移通常会创建表和索引。后续迁移可能会添加或删除列、引入新索引或创建新表。根据迁移工具的不同,迁移可能采用 SQL 语句或程序代码的形式,它们将转换为 SQL 语句。

我们现在拥有实体类User:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
​
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
​
  @Column({ name: 'first_name' })
  firstName: string
​
  @Column({ name: 'last_name' })
  lastname: string 
}
复制代码

然后我们在TypeORM中创建新迁移:

typeorm migration:create -n new_migrate
// new_migrate为迁移名称,可随意起
复制代码

它将生成一个migrate文件:

image-20210906143151505.png

这里的up是包含执行迁移的所有代码,down则用于恢复上次的迁移。QueryRunner可以执行所有数据库操作。

我们将lastName改成name字段,在这个文件写入:

import {MigrationInterface, QueryRunner} from "typeorm";
​
export class newMigrate1630909887647 implements MigrationInterface {
​
    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "lastName" RENAME TO "name"`);
    }
    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "name" RENAME TO "lastName"`);
    }
}
复制代码

更改数据库架构后,需要执行新迁移:

typeorm migration:generate -n rename_name
复制代码

migrate文件夹下生成新文件:

2.png

我们一旦执行了迁移并更改了数据库模式,就必须更新实体和映射器类,以适应新的更改。这意味需要向User实体类将lastName更改为name:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
​
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
​
  @Column({ name: 'first_name' })
  firstName: string
​
  @Column({ name: 'last_name' })
  name: string // 将lastName更改为name
}
复制代码

这时数据库的user表的lastName字段被更改为name字段:

3.png

对于ORM来说,工作流程包括使用迁移工具创建迁移,然后更新相应的实体和映射器类,这些更改都是手动应用的,所以同步更改可能是一个挑战。Prisma采取了不同的办法,它使用了Prisma Migrate来消除了这个同步问题( Django 的makemigrations 也类似的消除了同步问题)。

Prisma Migrate 是用于声明性数据建模和迁移的 CLI。与作为 ORM 一部分的大多数迁移工具不同,您只需要描述当前模式,而不是从一种状态移动到另一种状态的操作。Prisma Migrate 会推断操作、生成 SQL 并执行迁移。

对于数据库架构的任何进一步更改,只要再次运行prisma migrate指令,那么Prisma 客户端会自动重新生成,然后更新架构。

Prisma Migrate 使用 Prisma 模式更改自动生成完全可定制的数据库模式迁移有以下特点:

  • 迁移是自动生成的,所以不需要手动编写SQL。
  • Migrate 生成 SQL 迁移,确保迁移将始终导致跨环境的相同数据库架构。
  • 生成的 SQL 迁移可以完全自定义,可以完全控制确切的更改。

比如,我们先在prisma定义一个数据模型:

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}
复制代码

执行:

prisma migrate dev --name init
// init是迁移名称,可随意填写
复制代码

执行后,会生成新文件夹:

4.png

生成的目录结构为:

migrations/
  └─ 20210906023619_init/
    └─ migration.sql
复制代码

生成的文件夹是数据模型历史的真实数据来源。这时,prisma架构就与数据库架构同步了,并且初始化了迁移。

当然可以继续进行迁移:在user表中加入一个字段:

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  phone Int? // 新加入的字段
}
复制代码

执行:

prisma migrate dev --name add_phone
复制代码

生成文件:

5.png

此时,Prisma 架构再次与数据库架构自动同步,并且migrate文件夹中包含两次历史迁移:

migrations/
  └─ 20210906023619_init/
    └─ migration.sql
  └─ 20210906024134_added_phone/
    └─ migration.sql
复制代码

打开数据库发现:此时prisma已经将phone字段自动更新至数据库中。 6.png

这里有个要注意的点是:对于自动生成的文件不要随意进行编辑修改,比如,我们在其中一个migrate.sql中修改:

CREATE TABLE `User` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `email` VARCHAR(500) NOT NULL, //将191修改成500
    `name` VARCHAR(191),
​
    UNIQUE INDEX `User.email_unique`(`email`),
    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
复制代码

执行后,prisma会询问:

8.png

这说明prisma migate已经检测到迁移并且已经被更改了,并且 Prisma Model与迁移历史不同步,询问是否要重置?确定则会重置数据库并且重新放置所有的迁移(包括编辑的迁移),那么当前的数据库数据则会全部丢失。

原始SQL

使用ORM若在某些情况下需要执行原始SQL查询时,TypeORM可以选择使用QueryBuilder:

import {getConnection} from "typeorm";
await getConnection()
   .createQueryBuilder()
   .update(User)
   .set({ 
       firstName: "Timber", 
       lastName: "Saw",
       age: () => "'age' + 1"
   })
   .where("id = :id", { id: 1 })
   .execute();
复制代码

在prsima中,Prisma Client 公开了两种方法,可以原始 SQL 查询发送到您的数据库:

  • 使用$queryRaw返回的实际记录(例如,使用SELECT)
  • 使用$executeRaw返回受影响的行数(例如,之后UPDATE或DELETE)
const email = 'edna@prisma.io'
const result =
  await prisma.$queryRaw`SELECT * FROM User WHERE email = ${email};`
​
const result: number = await prisma.$executeRaw(
  'UPDATE User SET active = true WHERE emailValidated = true;'
)
复制代码

类型安全

TypeOrm是nodejs生态中首批完全采用TypeScript的ORM之一,并且在为其数据库查询中在一定级别的类型安全方面做的非常出色,但是在很多情况下 ,TypeORM的还是无法完全保证类型安全,比如,我们让hiredUsers数组中的每个对象在运行时只返回id和name,但是TypeScript编译器却对此一无所知,通过打点我们可以访问到实体上定义的所有属性:

9.png

于是,运行该代码,就会报错:

10.png

这是因为TypeScript 编译器只看到User返回对象的类型,但不知道这些对象在运行时实际携带的字段。因此,它无法保护您访问尚未在数据库查询中检索到的字段,从而导致运行时错误。

但是在prisma中,Prisma Client 可以在相同情况下保证完全类型安全,并保护您免于访问未从数据库中检索到的字段:

11.png

我们对post打点发现,prisma只允许我们访问publishedPosts返回携带的字段,我们若继续访问,则TypeScript 编译器将在编译时抛出以下错误:

12.png

这是因为Prisma Client会动态地为其查询生成返回类型。

当然还有其他类型安全方面、以及API的不同,欢迎大家亲自去体验,这里就不一一列举了。

可视化工具

Prisma Studio 是数据库中数据的可视化编辑器。这是prisma自带的简单表格界面,我们可以利用这个快速查看本地数据库并进行CRUD操作以及过滤、分页等。

13.png

Prisma作为新型的ORM,它虽然本质还是ORM,但它比传统的ORM要强大的多: Prisma 让开发人员更有效率 图中可以看出:它的生产力会比传统的ORM要高。

它为数据访问提供了一种类型安全并现代化的API,以确保开发者可以用方便、高效且安全的方式读取和写入数据,节省大量时间。

与任何其他工具一样,它有自己的权衡。以下将为prisma做一个简单介绍。

构成部分

prisma Client

Prisma Client JS是基于 数据模型定义 (它是数据库架构的声明性表示)自动生成的类型安全的数据库客户端。prisma的核心就是这个,这是自动生成的代码,能够进行数据的安全存取,这个就相当于ORM。

从技术角度来说,prisma Client由三个主要组件组成:

  • JavaScript 客户端库

  • TypeScript 类型定义

  • 一个查询引擎(以二进制文件的形式)

    • 查询引擎的工作流程:

      运行时查询引擎的典型流程

    1.调用$connect()启动查询引擎进程。

    2.查询引擎建立与数据库连接并创建连接池。

    3.prismsClient已准备好向数据库发送查询。

    4.prismsClient向查询引擎发送一个findMany()查询。

    5.查询引擎将该查询翻译成SQL并发送给数据库。

    6.查询引擎接收来自数据库SQL的响应。

    7.查询引擎将结果用JS对象返回给prismsClient。

    8.调用$disconnect()。

    9.查询引擎关闭数据库连接。

    10.查询引擎进程停止。

Prisma Migrate

它可以将Prisma模式转换成所需的SQl,以创建与更改数据库中的表。它使用强大的SDL语法进行直观的数据建模和无缝的数据库迁移来管理数据库结构。

Prisma Studio

它能够以图形界面展示数据库,其实就是一个数据库的gui。

基础知识

schema.prisma

schema.prisma是 Prisma 的主要配置文件,使用vscode可配合Prisma插件进行使用(高亮、语法检查、格式化),会让开发者的体验感会更好。schema.prisma由以下部分构成:

  • 数据源:prisma如何连接到数据源。节约学习成本可使用文件形式的sqlite:

    datasource db {
      provider = "sqlite"
      url      = "file:./dev.db"
    }
    复制代码

    或者连接本地Mysql或其他数据库:

    datasource db {
      provider = "mysql"
      url      = "mysql://root:root1234@localhost:3306/prisma"
    }
    复制代码
  • 生成器(可选):基于哪个数据模型来生成客户端。

    generator client {
      provider = "prisma-client-js"
    }
    复制代码
  • 数据模型定义:说明你的应用程序模型。对于关系型数据库:用model映射了一张表;非关系型数据库:用model映射到一个集合/资源上。

    model User {
      id    Int     @id @default(autoincrement())
      email String  @unique
      name  String?
      posts Post[]
    }
    复制代码
    • 使用PrismaClient时,Prisma模式中的每个模型都被转换成专用的TypeScript类型,以使数据库访问完全类型安全。

    • 类型修饰符:

      • [ ]:将字段设为列表
      • ?:将字段设为可选。不使用?类型修饰符注释字段时,模型每条记录都需要该字段。
    • 属性定义:

      • @id:定义id字段。

        • 复合id:@@id([firstName, lastName])
      • @default:定义默认值。

        • 表示底层数据库(仅关系数据库)中的默认值

        • 使用 Prisma 函数。例如cuid()和uuid()由Prisma的公司提供查询函数:

          • cuid():根据cuid规范生成全局唯一标识符。
          • uuid():根据UUID规范生成全局唯一标识符。
          • autoincrement():创建一个整数序列,并根据该序列将递增的值分配给创建的记录的 ID 值。
          • now():设置记录创建时间的时间戳。
      • @unique:唯一标识符。

    表之间的关联,用外键进行约束。

    model Post {
      id        String @id @default(cuid())
      title     String
      content   String?
      published Boolean @default(false)
      author    User?   @relation(fields: [authorId], references: [id])
      authorId  Int?
    }
    复制代码
    • Prisma 中有三种不同类型(或基数)的关系:有一对一、一对多、多对多的关系。例子是一对多的关系。在 Prisma 模式中,外键/主键关系由字段@relation上的属性表示:

      • fields属性位于当前表中,references位于关系另一方中。
      • authorId是Post的外键,id是User的主键。
      • 除了fields和references,还可以使用name定义关系名称来避免歧义。
      • 关系若是一对一、一对多,那么@relation是必须使用的。多对多可以不使用@relation来声明关系,这样会使用双方的@id来建立级联关系。
      • 当然也可以关联自己的模型,这种关系被称为“自我关系”。自我关系也始终需要@relation属性。

CRUD 操作

  • findMany:获取所有记录

  • findUnique:通过唯一标识符或id返回单个记录

  • create:创建字段

    • 例如:

      const user = await prisma.user.create({
        data: {
          email: '1@cc.com',
          name: 'deserve',
        },
      })
      复制代码
  • update:更新单条记录

  • upsert:更新或创建记录

  • delete: 删除单个记录

  • updateMany: 更新多条记录

  • deleteMany: 删除多条记录

  • where:过滤

  • orderby:排序

  • skip、take:分页

    • 例如: 返回4-7的记录

      const results = await prisma.post.findMany({
        skip: 3,
        take: 4,
      })
      复制代码

      不会在数据库级别进行扩展。读取记录会遍历所有跳过的记录,若数据量大,会造成性能负面影响。

  • aggregate:平均值

    • 例如:

       const aggregations = await prisma.user.aggregate({
          _avg: {
            age: true,
          },
        })
        
        console.log('Average age:' + aggregations._avg.age)
      复制代码
  • groupBy:聚合

    • where:过滤单个记录
    • having:过滤数据组

这里只简单列出部分,更多api可参考官方文档。

Prisma.validator

它采用生成的类型,并且返回一个类型安全的对象,该对象遵循生成的类型模型字段。

如需要返回用户id为3的email,如用户不存在,则返回null:

import { Prisma } from '@prisma/client'
​
const userEmail: Prisma.UserSelect = {
  email: true,
}
​
const user = await prisma.user.findUnique({
  where: {
    id: 3,
  },
  select: userEmail,
})
复制代码

用这种方法很有效,但是如果将鼠标悬停在userEmail上,发现TypeScript无法推断出对象的键或值: 14.png

或者我们在findUnique中对userEmail打点,则会看到select对象可用的所有属性:

15.png

它是类型化的,但不是类型安全的,它依然可以访问其他可用的属性。

我们可以使用prisma.validator来验证生成的类型以确保它们是类型安全的:

import { Prisma } from '@prisma/client'
​
const userEmail = Prisma.validator<Prisma.UserSelect>()({
  email: true,
})
​
const user = await prisma.user.findUnique({
  where: {
    id: 3,
  },
  select: userEmail,
})
复制代码

鼠标悬停userEmail上,

16.png

在findUnique中对userEmail打点,则只能看到email了:

17.png

这样就将UserSelect生成的类型传递给Prisma.validator,userEmail现在就是类型安全的。

Prisma.UserGetPayload

若需要一种类型仅传递User的email和name字段时,为保证类型安全且易维护,可使用Prisma.UserGetPayload:

import { Prisma } from '@prisma/client'
​
const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
  include: { posts: true },
})
​
const userPersonalData = Prisma.validator<Prisma.UserArgs>()({
  select: { email: true, name: true },
})
​
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
复制代码

这里使用Prisma.validator来创建两个类型安全对象,然后使用Prisma.UserGetPayload实用程序函数来创建可用于返回所有用户及其帖子的类型。

18.png

PRISMA+RESTFUL

19.png

REST,即Representational State Transfer表现层状态转,本质上就是用来定义URI,通过API来获取资源。通用系统架构,不受语言限制。

用prisma来创建RestApi的,它拥有类型安全、先进 API 以及自由读写关系型数据等诸多优点。在构建 REST API 时,可以在路由中使用 Prisma Client来发送数据库查询。

带有 Prisma 客户端的 REST API

这里使用express框架来实现Rest+Prsima。

引入PrismaClient与express,并将PrismaClient实例化:

import { PrismaClient } from "@prisma/client";
import express from "express";
​
const prisma = new PrismaClient();
const app = express();
​
app.use(express.json());
复制代码

依照prisma预设的数据模型,定义一些接口:

// 查询所有用户
app.get('/users', async (req, res) => {
  const result = await prisma.user.findMany();
  res.json(result);
});
// 新建文章
app.post(`/post`, async (req, res) => {
  const { title, content, authorEmail } = req.body;
  const result = await prisma.post.create({
    data: {
      title,
      content,
      author: {
        connect: {
          email: authorEmail,
        },
      },
    },
  });
​
  res.json(result);
});
...
复制代码

定义express启动端口:

app.listen(3000, () =>
  console.log(`🚀 Server ready at: http://localhost:3000`)
);  
复制代码

启动express脚手架,调用接口,便可得到数据。

PRISMA+GRAPHQL

航天飞机

graphql是来自于Facebook,并于2015年开源,是一个用于 API 的查询语言,是基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。就比如SQL是结构化查询语言的简称,所以Graph+QL = 图表化(可视化)的查询语言,是一种描述客户端如何向服务端请求数据的API语法。它本身并不是数据库,只是作用于数据的接口,不会直接操作数据库(所以叫做操作API),它只是能让你准确获得你所想要的数据。

与restful不同的是:restful一个接口只能返回一个资源,graphql一次可以获取多个资源。并且rest用不同url来区分资源。graphql用类型来区分资源。它通常用作 RESTful API 的替代品,但也可以用作现有 RESTful 服务之上的附加“网关”层。

而prisma是一个数据库抽象层,可以将数据库转换为带有CRUD操作和实时功能的GraphQL API。它是数据库和GraphQL服务器之间的粘合剂。Prisma client 能够很好的兼容 Apollo 框架生态(它是一个开源的、符合规范的 GraphQL 服务器。也是目前使用最广的GraphQL服务端框架之一。)拥有默认的对 GraphQL subscriptions 的支持,以及 Relay 风格的分页支持,同时提供端对端类型安全的以及内置的 dataloader 来解决 N+1 问题(关于DataLoader,可参考GraphQL N+1问题到DataLoader源码解析。)

gql-api.png

我们可以将预设的Prisma Client挂载在Context中(context.ts):

import { PrismaClient } from '@prisma/client'
​
const prisma = new PrismaClient()
​
export interface Context {
  prisma: PrismaClient
}
​
export const context: Context = {
  prisma: prisma
}
复制代码

配置ApolloServer服务:

import { ApolloServer } from "apollo-server";
import { Context, context } from "./context";
const server = new ApolloServer({ typeDefs, resolvers, context });
复制代码

我们向ApolloServer添加了graphlL 类型定义 typeDefs、解析器以及prisma作为数据源传入。

Apollo Server为每个传入操作都调用这个数据源。该context对象在服务器的解析器之间传递。通过该方法可以从共享context对象访问prisma数据源并使用它们来获取数据。这样就不用为每个Resolver导入一次Prisma Client。

定义 typeDefs:

const typeDefs = `
  type Query {
    allUsers: [User!]!
    draftsByUser(id: Int!): [Post]
  }
​
  type Mutation {
    signupUser(name: String, email: String!): User!
    createDraft(title: String!, content: String, authorEmail: String): Post
  }
​
  type User {
    id: Int!
    email: String!
    name: String
    posts: [Post!]!
  }
​
  type Post {
    id: Int!
    createdAt: DateTime!
    updatedAt: DateTime!
    title: String!
    content: String
    published: Boolean!
    viewCount: Int!
    author: User
  }
​
  scalar DateTime
`;
复制代码

配置解析器函数:

const resolvers = {
  Query: {
    allUsers: (_parent, _args, context: Context) => {
      return context.prisma.user.findMany()
    },
    draftsByUser: (_parent, args: { id: number }, context: Context) => {
      return context.prisma.user.findUnique({
        where: {
          id: args.id
        }
      }).posts({
        where: {
          published: true
        }
      })
    },
    },
   Mutation: {
    createDraft: (
      _parent,
      args: { title: string; content: string | undefined; authorEmail: string },
      context: Context
    ) => {
      return context.prisma.post.create({
        data:{
          title: args.title,
          content: args.content,
          author: {
            connect: {
              email: args.authorEmail
            }
          }
        }
      })
    },
  }
  }
复制代码

对应的查询语句:

-----------------查询所有用户
{
  allUsers {
    id
    name
    email
    posts {
      id
      title
    }
  }
}
-----------------查询某个用户的所有未发布的文章
{
  draftsByUser(id: 3) {
    id
    title
    content
    published
    viewCount
    author {
      id
      name
      email
    }
  }
}
-----------------新建文章
mutation {
  createDraft(title: "Hello World", authorEmail: "burk@prisma.io") {
    id
    published
    viewCount
    author {
      id
      email
      name
    }
  }
}
复制代码

解析器可以选择接受四个位置参数:(parent, args, context, info),这个context是在为特定操作执行的所有解析器之间共享的对象,也就是prisma对象。

参考链接

文章分类
前端
文章标签