GraphQL 是一种用于 API 的查询语言,同时也是一个运行时(runtime)用来处理这些查询的类型系统。它由 Facebook 在 2012 年开发,并于 2015 年公开发布。GraphQL 提供了一种更高效、强大和灵活的替代方案,相比于传统的 REST API。
核心特性
- 请求你所需要的数据:不多也不少。客户端可以精确地指定它需要哪些数据,而服务器则返回这些数据,避免了过度获取(over-fetching)或者获取不足(under-fetching)的问题。
- 获取多个资源只用一个请求:GraphQL 查询不仅可以获取资源的属性,还可以顺着资源之间的连接一起获取,这意味着你可以通过一个查询来获取所有所需数据,而不是像 REST API 那样需要多个请求。
- 描述所有的可能类型系统:GraphQL API 基于强类型系统构建,每个 API 都会定义一个类型系统,由各种类型(type)组成,这些类型描述了你可以从该 API 查询的数据。
- 动态查询:查询是在运行时发送的,不是由服务器预定义的,这给客户端提供了极大的灵活性。
- 自动生成接口文档且有非常强大的接口调试工具,和 Nest.js 结合也很完美。
工作原理
GraphQL 的工作原理是通过定义一系列的类型,这些类型描述了可以从服务器获得的数据结构。这些类型包括对象类型(表示资源)、标量类型(如 Int
、Float
、String
等基本类型)、枚举类型等。
客户端在发送查询(query)或者变更(mutation)时,会指定它需要哪些字段。服务器会根据查询和内部逻辑解析这些字段,并返回相应的数据结构。
示例
假设我们有一个图书管理系统,我们想要查询一本书的标题和它的作者信息。在 GraphQL 中,我们可以这样写查询:
graphql
{
book(id: "1") {
title
author {
name
age
}
}
}
服务器会返回一个 JSON 对象,其中包含了书的标题和作者的名字及年龄。
与 REST 的比较
与 RESTful API 相比,GraphQL 允许客户端指定他们需要的确切数据,减少了数据传输,可能会提高性能。此外,GraphQL 使得维护和扩展 API 更为简单,因为添加新的字段和类型不会影响现有的查询。
使用场景
GraphQL 适用于复杂系统中的数据交互,尤其是当客户端需要从多个资源中获取数据时。它已经被许多大型公司和服务采用,包括 Facebook、GitHub、Shopify 等。
总结而言,GraphQL 提供了一种强大的方式来与 API 交互,它让客户端能够以一种更高效和精确的方式获取数据,同时简化了服务器端的数据管理。
业务分层和数据对象
这张图展示了在软件架构中常见的几种对象类型以及它们之间的关系。这些对象通常用于企业级应用程序中,特别是在分层架构模式下。我来依次解释每一个概念:
- 视图对象(View Object,VO): 视图对象是指在表示层使用的对象,它通常包含了用户界面所需显示的数据。VO 是从客户端的角度设计的,用于适配用户界面的需要,通常它们是从服务层获得的数据的子集。
- 数据传输对象(Data Transfer Object,DTO): 数据传输对象用于跨进程或网络传输数据。DTO 通常是一个平面数据结构,用于封装数据,而不包含任何业务逻辑。它用于服务层和表示层之间的数据传输,可以减少网络调用的次数,因为它允许一次性传输多个数据项。
- 领域对象(Domain Object,DO): 领域对象是指反映业务领域内概念的对象。它们包含了业务逻辑和业务状态信息,是实现业务规则和业务行为的关键组件。在服务层中,领域对象用于执行具体的业务操作。
- 持久对象(Persistent Object,PO): 持久对象是指那些被映射到数据库表的对象,它们通常由数据访问对象(Data Access Object,DAO)管理。PO 包含了数据持久化的状态信息,通常与数据库中的表结构相对应。
整体来看,这张图说明了在典型的分层架构中数据是如何从数据库层(通过 PO)经过业务逻辑层(DO 和 DTO)最终展示到用户界面(VO)的过程。每一层都使用特定类型的对象来处理数据和逻辑,以保持架构的清晰和解耦。
VO 需要的和 DTO 定义的有时并相等,所以这时候 GraphQL 的价值就体现出来了,VO 可以根据需要动态获取自己想要的数据。
Nest.js 的 DTO(数据传输对象)和 DAO(数据访问对象)
DTO 是一个对象,它定义了如何通过网络发送数据。在 Nest.js 中,DTO 通常用于定义客户端和服务器之间传输的数据结构。DTO 通常用来映射请求体(request body)中的数据,它们可以帮助确保输入数据的有效性,并且可以作为 TypeScript 类或接口来实现。DTO 不应该包含任何业务逻辑,它们的主要目的是数据传输。我们定义一个 DTO:
typescript
export class CreateUserDto {
readonly name: string;
readonly email: string;
readonly password: string;
}
接下来,我们将定义一个 DAO(数据访问对象)类,它将用于与数据库进行交互:
typescript
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
@Column()
password: string;
}
在这个例子中,我们使用 TypeORM 作为我们的 ORM 框架。我们定义了一个 User 实体,它映射到数据库中的一个表。我们使用装饰器来定义实体的属性和关系
接下来,我们将使用 DTO 和 DAO 类来创建一个用户:
typescript
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = new User();
user.name = createUserDto.name;
user.email = createUserDto.email;
user.password = createUserDto.password;
return await this.userRepository.save(user);
}
}
在这个例子中,我们使用了 Nest.js 的依赖注入功能来注入 UserRepository。我们使用 create() 方法来创建一个新的用户,并将其保存到数据库中。
在 Nest.js 中使用 GraphQL 实现 CRUD 操作需要遵循以下步骤:
Nest 集成 GraphQL
安装必要的依赖、初始化项目
初始化项目:
安装四个包:
bash
npm i graphql @nestjs/graphql apollo-server-express @nestjs/apollo
- graphql:这是一个JavaScript库,用于构建 GraphQL 服务器。它提供了定义 GraphQL 模式(Schema)的语法,执行查询和变更,以及与 GraphQL 类型系统交互的核心功能。
- @nestjs/graphql:这是为 NestJS 框架定制的包,NestJS 是一个用于构建高效、可靠的服务器端应用程序的框架。这个包提供了与 NestJS 无缝集成的功能,允许你在 NestJS 应用程序中以模块化和易于维护的方式构建GraphQL API。
- apollo-server-express:这是一个集成了 Apollo Server 和 Express.js 的库,使得你可以在 Express.js 的 Web 服务器上快速搭建一个 GraphQL 服务器。Apollo Server 是一个开源的 GraphQL 服务器,它简化了 GraphQL API 的设置,提供了性能监控、错误跟踪和其他有用的功能。
- @nestjs/apollo:这个包提供了 Apollo Server 与 NestJS 框架集成的功能。通过这个集成,开发者可以利用 Apollo Server 的强大功能,如性能监控和错误跟踪,同时享受 NestJS 提供的模块化架构和依赖注入系统。
注册 GraphQLModule
nest generate module user 生成对应的模块测试。
在你的 Nest.js 应用中设置 GraphQL 模块。app.module.ts
:
typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
import { UserModule } from './user/user.module';
@Module({
imports: [
GraphQLModule.forRoot({
driver: ApolloDriver,
autoSchemaFile: true,
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
在这里,autoSchemaFile
属性指定了自动生成的 GraphQL schema 文件的位置。Nest.js 会根据你的代码自动生成 schema。
autoSchemaFile**** 也可以设置为 true ,这意味着生成的模式将保存到内存中。
创建 GraphQL 类型、输入和解析器(Resolver)
在 user.resolver.ts
文件中,你可以定义 GraphQL 查询和变更操作。这里是一个简单的 CRUD 操作例子:
操作类型:
- @Query 查询
- @Mutation 更新
typescript
import { Query, Mutation, Args, Resolver } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query(() => [User])
async users(): Promise<User[]> {
return this.userService.findAll();
}
@Query(() => User)
async user(@Args('id') id: number): Promise<User> {
return this.userService.findOne(id);
}
@Mutation(() => User)
async createUser(
@Args('createUserInput') createUserInput: CreateUserInput,
): Promise<User> {
return this.userService.create(createUserInput);
}
@Mutation(() => User)
async updateUser(
@Args('updateUserInput') updateUserInput: UpdateUserInput,
): Promise<User> {
return this.userService.update(updateUserInput.id, updateUserInput);
}
@Mutation(() => User)
async deleteUser(@Args('id') id: number): Promise<User> {
return this.userService.delete(id);
}
}
定义 dto 类型
CreateUserInput
在 Nest.js 中使用 GraphQL 时,我们通常会为每个操作定义一个输入类型(DTO - Data Transfer Object)。这些输入类型定义了前端可以用来与后端交互的数据结构。对于创建和更新用户操作,我们需要分别定义 CreateUserInput
和 UpdateUserInput
类型。
输入和输出类型:
- @InputType 输入参数对象
- @ObjectType 输出参数对象
这个输入类型用于创建用户时前端需要提供的数据。
typescript
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class CreateUserInput {
@Field()
name: string;
@Field()
email: string;
// 可以根据需要添加更多字段
}
在这个 CreateUserInput
类型中,我们定义了 name
和 email
两个字段,这些是创建新用户时必须提供的。
UpdateUserInput
这个输入类型用于更新用户信息时前端需要提供的数据。
typescript
import { InputType, Field, Float } from '@nestjs/graphql';
@InputType()
export class UpdateUserInput {
@Field(() => Float)
id: number;
@Field({ nullable: true })
name?: string;
@Field({ nullable: true })
email?: string;
// 可以根据需要添加更多字段或者使某些字段为可选
}
在 UpdateUserInput
类型中,我们定义了 id
字段来指定要更新的用户,以及 name
和 email
字段作为可更新的属性。这里的字段都是可选的(使用 nullable: true
),这意味着在更新操作中,可以只更新部分字段。
以上就是 CreateUserInput
和 UpdateUserInput
类型的定义。在实际的项目中,你可能需要根据业务需求来调整这些输入类型的字段。在 GraphQL 中,使用这种输入类型可以帮助我们确保前端发送的数据结构是我们期望的格式,并且可以在后端进行相应的验证。
返回 User
在 user.entity.ts
文件中,你可以这样定义:
typescript
import { Field, ObjectType, ID } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(() => ID)
id: number;
@Field()
name: string;
@Field()
email: string;
}
实现服务(Service)
服务层包含实际的业务逻辑。在 user.service.ts
文件中,你可以实现 CRUD 方法:
typescript
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
@Injectable()
export class UserService {
private users: User[] = []; // 这里简单使用数组模拟数据库
findAll(): User[] {
return this.users;
}
findOne(id: number): User {
return this.users.find((user) => user.id === id);
}
create(createUserInput: CreateUserInput): User {
const user = { id: Date.now(), ...createUserInput };
this.users.push(user);
return user;
}
update(id: number, updateUserInput: UpdateUserInput): User {
const userIndex = this.users.findIndex((user) => user.id === id);
if (userIndex === -1) {
throw new Error('User not found');
}
this.users[userIndex] = { ...this.users[userIndex], ...updateUserInput };
return this.users[userIndex];
}
delete(id: number): User {
const userIndex = this.users.findIndex((user) => user.id === id);
if (userIndex === -1) {
throw new Error('User not found');
}
const user = this.users[userIndex];
this.users.splice(userIndex, 1);
return user;
}
}
user.module.ts:
typescript
import { Module } from '@nestjs/common';
import { UserResolver } from './user.resolve';
import { UserService } from './user.service';
@Module({
providers: [UserService, UserResolver],
})
export class UserModule {}
启动项目
npm run start:dev
启动项目后访问 [http://localhost:3000/graphql](http://localhost:3000/graphql)
就可以看到 graphql 的调试界面了:
在GraphQL中,你可以通过编写查询(query)和变更(mutation)来与服务器进行交互。
创建新用户
为了创建一个新用户,你可以使用一个 mutation,并且传递必要的参数:
graphql
mutation {
createUser(createUserInput: {
name: "Jane Doe",
email: "jane.doe@example.com"
}) {
id
name
email
}
}
再新增一个:
查询所有用户
graphql
query {
users {
id
name
email
}
}
查询特定 ID 的用户:
如果你想查询 ID 为 1702138576474 的用户:
graphql
query {
user(id: 1702138576474) {
id
name
email
}
}
更新用户
如果你想更新 ID 为 1702138368180 的用户的名称和电子邮件:
graphql
mutation {
updateUser(updateUserInput: {
id: 1702138368180,
name: "黛玉",
email: "yyy@yy.com"
}) {
id
name
email
}
}
删除用户
如果你想删除 ID 为 1702138368180 的用户:
graphql
mutation {
deleteUser(id: 1702138368180) {
id
name
email
}
}
在实际应用中,你需要根据你的 GraphQL 客户端或者库的指南来执行这些查询和变更。例如,如果你使用 Apollo Client,你会使用useQuery
和useMutation
hooks 来在 React 组件中执行这些操作。如果你在通过 HTTP 直接发送请求,你需要将这些查询和变更作为 POST 请求的 body 发送到 GraphQL 服务器的端点。
因为 GraphQL API 实际上发送的是 HTTP POST 请求。虽然 GraphQL 也可以通过 GET 请求来发送查询,但是大多数情况下,尤其是在执行查询(Query)、变更(Mutation)和订阅(Subscription)时,使用的是 POST 请求。
在一个标准的 HTTP POST 请求中,GraphQL 查询被放置在请求体(body)中,并以 JSON 格式发送。这个 JSON 对象通常包含以下几个字段:
query
: 这个字段包含了用字符串形式表示的 GraphQL 查询或变更。它可以包含多个字段和嵌套对象,用以指定客户端想要从服务器获取的数据结构。variables
(可选): 如果查询中包含任何变量,这个字段用于传递这些变量的值。这些值在查询中将替换对应的变量占位符。operationName
(可选): 当一个请求体中包含多个查询或变更时,这个字段用来指定要执行的具体操作的名称。
一个典型的 GraphQL POST 请求的请求体可能看起来像这样:
json
{
"query": "query GetUserInfo($userID: ID!) { user(id: $userID) { id, name, email } }",
"variables": { "userID": "1" },
"operationName": "GetUserInfo"
}
在这个请求中,客户端正在请求一个名为 GetUserInfo
的查询,它带有一个名为 $userID
的变量,这个变量在 variables
字段中被设置为 "1"
。
这个 POST 请求将通过 HTTP 协议发送到服务器的 GraphQL 端点(endpoint),通常是一个特定的 URL,例如 https://example.com/graphql
。
服务器接收到请求后,会解析 JSON 请求体,执行指定的 GraphQL 查询或变更,并将结果以 JSON 格式返回。返回的 JSON 通常包含 data
字段,其中包含了查询结果,以及如果有任何错误发生,还会包含一个 errors
字段。
请注意,虽然 GraphQL 通过 HTTP 发送请求和接收响应,但它并不依赖于任何特定的传输协议。HTTP 只是 GraphQL 最常用的传输协议之一。
前端集成 GraphQL
创建 react 项目:
安装:
bash
npm install @apollo/client graphql
graphql 使用步骤如下:
初始化 ApolloClient,创建 utils/apollo.ts:
typescript
import { ApolloClient, InMemoryCache } from '@apollo/client';
export const client = new ApolloClient({
uri: 'http://localhost:3000/graphql',
cache: new InMemoryCache(),
});
包裹 ApolloProvider,在 main.tsx 文件:
tsx
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import { client } from './utils/apollo';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
查询使用 useQuery,变更使用 useMutation,具体如下:
查询所有用户
javascript
import { gql, useQuery } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function UsersList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return (
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}
查询单个用户
javascript
import { gql, useQuery } from '@apollo/client';
const GET_USER = gql`
query GetUser($id: Float!) {
user(id: $id) {
id
name
email
}
}
`;
function User({ userId }) {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: userId },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return (
<div>
<h3>{data.user.name}</h3>
<p>{data.user.email}</p>
</div>
);
}
创建用户
javascript
import { gql, useMutation } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($createUserInput: CreateUserInput!) {
createUser(createUserInput: $createUserInput) {
id
name
email
}
}
`;
function CreateUserForm() {
let nameInput, emailInput;
const [createUser, { data, loading, error }] = useMutation(CREATE_USER);
if (loading) return 'Submitting...';
if (error) return `Submission error! ${error.message}`;
return (
<form
onSubmit={e => {
e.preventDefault();
createUser({
variables: {
createUserInput: {
name: nameInput.value,
email: emailInput.value,
},
},
});
nameInput.value = '';
emailInput.value = '';
}}
>
<input
ref={node => {
nameInput = node;
}}
placeholder="Name"
/>
<input
ref={node => {
emailInput = node;
}}
placeholder="Email"
/>
<button type="submit">Create User</button>
</form>
);
}
更新用户
javascript
import { gql, useMutation } from '@apollo/client';
const UPDATE_USER = gql`
mutation UpdateUser($updateUserInput: UpdateUserInput!) {
updateUser(updateUserInput: $updateUserInput) {
id
name
email
}
}
`;
function UpdateUserForm({ userId }) {
let nameInput, emailInput;
const [updateUser, { data, loading, error }] = useMutation(UPDATE_USER);
if (loading) return 'Updating...';
if (error) return `Update error! ${error.message}`;
return (
<form
onSubmit={e => {
e.preventDefault();
updateUser({
variables: {
updateUserInput: {
id: userId,
name: nameInput.value,
email: emailInput.value,
},
},
});
}}
>
<input
ref={node => {
nameInput = node;
}}
placeholder="Name"
/>
<input
ref={node => {
emailInput = node;
}}
placeholder="Email"
/>
<button type="submit">Update User</button>
</form>
);
}
删除用户
javascript
import { gql, useMutation } from '@apollo/client';
const DELETE_USER = gql`
mutation DeleteUser($id: Float!) {
deleteUser(id: $id) {
id
}
}
`;
function DeleteUserButton({ userId }) {
const [deleteUser, { data, loading, error }] = useMutation(DELETE_USER);
if (loading) return 'Deleting...';
if (error) return `Deletion error! ${error.message}`;
return (
<button
onClick={() => {
deleteUser({ variables: { id: userId } });
}}
>
Delete User
</button>
);
}
在上述代码中,我们使用了 useQuery
钩子来执行查询操作,useMutation
钩子来执行变更操作。每个操作都定义了一个 GraphQL 操作字符串,它包含了必要的查询或变更,并指定了所需的参数(如果有的话)。在组件中,我们通过传递相应的变量来执行这些操作,并处理可能的加载状态、错误和响应数据。