使用React和StepZen进行第一次GraphQL应用开发
首先
本文将介绍如何使用 StepZen 将 PostgreSQL 数据转换为 GraphQL API,并使用 React 和 Next 创建 Web 应用程序。
optimisuke/purchase-history
对于预定阅读。
-
- Web アプリについて、基本的なことを理解している方を想定しています。
-
- また、Node.js (v18.17.0) がインストールされていることを前提としています。
- コマンドライン環境についても実行できることを想定していますので、Windowsであれば、WSL (Windows Subsystem for Linux) 等をご準備ください。
创建的东西

从下一章开始,我们将分三个步骤来说明。
-
- 使用React/Next来创建显示界面
-
- 使用StepZen来创建GraphQL API
- 调用GraphQL API
使用React/Next来创建显示界面。
前往工作文件夾,执行以下命令以配置Next环境。
在本文中,我们将操作文件夹设为purchase-history。
npx create-next-app app -ts
打开应用程序文件夹,删除不需要的文件。
cd app
rm -rf app public
这次为了使用React的UI组件库MUI,我们需要安装相关的库。
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
由于准备已经就绪,我们将开始创建所需的文件。
这次,我们假设要构建一个简单的页面,所以我们将在以下的pages/index.tsx中构建页面。
实际上,根据需要,我们可能需要将文件分开为不同的组件。
mkdir pages
在下面的文件中,显示了头部和订单表部分。
订单表部分会为每个订单创建一个表格,并将包含在订单中的产品显示为表格的行。
在展开数组时,我们利用了数组方法.map()。
// pages/index.tsx
import {
AppBar,
Avatar,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Toolbar,
Typography,
} from "@mui/material";
import type { NextPage } from "next";
type Order = {
id: number;
createdat: string;
lineitem: Lineitem[];
};
type Lineitem = {
product: Product;
};
type Product = {
id: number;
title: string;
image: string;
};
const Home: NextPage = () => {
const orders: Order[] = [
{
id: 1,
createdat: "2023-09-25",
lineitem: [
{
product: {
id: 1,
title: "Apple",
image: "apple-image-url",
},
},
{
product: {
id: 2,
title: "Banana",
image: "banana-image-url",
},
},
],
},
{
id: 2,
createdat: "2023-09-25",
lineitem: [
{
product: {
id: 1,
title: "Tomato",
image: "tomato-image-url",
},
},
],
},
];
return (
<div>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Purchase History
</Typography>
</Toolbar>
</AppBar>
<div>
{orders.map((order: Order) => (
<Paper key={order.id} style={{ marginBottom: "16px" }}>
<Typography variant="h6" style={{ padding: "16px" }}>
Order ID: {order.id}
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell style={{ width: 20 }}>Image</TableCell>
<TableCell style={{ width: 1000 }}>Name</TableCell>
</TableRow>
</TableHead>
<TableBody>
{order.lineitem.map((lineitem) => (
<TableRow key={lineitem.product.id}>
<TableCell>
<Avatar
variant="square"
src={lineitem.product.image}
sx={{ width: 100, height: 100 }}
/>
</TableCell>
<TableCell>{lineitem.product.title}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
))}
</div>
</div>
);
};
export default Home;
文件夹的组成如下。
.
├── README.md
├── next-env.d.ts
├── next.config.js
├── node_modules
├── package-lock.json
├── package.json
├── pages
│ └── index.tsx
├── postcss.config.js
├── tailwind.config.ts
└── tsconfig.json
保存上述的代码后,执行以下命令。
npm run dev
在浏览器中访问localhost:3000,将显示如下界面。

接下来,将使用StepZen创建GraphQL API,并随后添加调用该GraphQL API的实现。
使用StepZen创建GraphQL API。
使用中的数据库配置
本次我们将使用准备好的PostgreSQL数据库来进行StepZen的教程实施。
您可以使用以下连接信息连接到要使用的数据库。
host: postgresql.introspection.stepzen.net
username: testUserIntrospection
password: HurricaneStartingSample1934
database name: introspection
这个数据库使用了6个表,包括客户(customer)、地址(address)、订单(order)、商品(products)、客户和地址的中间表(customeraddress)以及订单和商品的中间表(lineitem)。这些表之间的关系如下所示。
顾客(customer)和地址(address)之间存在多对多的关系,通过中间表(customeraddress)进行关联。
顾客(customer)和订单(order)之间存在一对多的关系。
订单(order)和商品(product)之间存在多对多的关系,通过中间表(lineitem)进行关联。
我們將在以下圖中展示實體關係圖(ER圖)。

安装和设置 StepZen CLI
请按照 StepZen 官方文档(StepZen 安装和设置)的步骤,首先安装 StepZen CLI。如果已经安装了 Node.js,可以使用 npm install -g stepzen 来安装。然后,创建 StepZen 账户并从仪表板获取 Admin Key,使用 stepzen login 命令进行登录。
使用StepZen创建GraphQL API
按照StepZen官方文档(使用@dbquery自定义指令来入门PostgreSQL数据库)的指引,首先我们要从数据库的模式信息生成GraphQL的配置文件。
和之前的项目一样,在名为purchase-history的工作文件夹中,执行以下命令。
mkdir api
cd api
stepzen import postgresql
当执行最后一条命令时,会显示以下类似的问题,并以对话形式回答有关数据库连接信息等问题。
? What would you like your endpoint to be called? api/opining-condor
? What is your host? postgresql.introspection.stepzen.net
? What is the username? testUserIntrospection
? What is the password? [hidden]
? What is your database name? introspection
? What is your database schema (leave blank to use defaults)?
? Automatically link types based on foreign key relationships using @materializer
(https://stepzen.com/docs/features/linking-types) Yes
Starting... done
通过在最后一个问题上选择”是”,可以使用 @materializer(https://stepzen.com/docs/features/linking-types)根据外键关系自动链接类型,并将其反映在层级化的GraphQL定义中,从而实现外键约束的关联。
执行上述命令后,将会生成以下文件。
purchase-history/api
├── config.yaml
├── index.graphql
├── postgresql
│ └── index.graphql
└── stepzen.config.json
在 StepZen 的设置文件中,有一个名为 stepzen.config.json 的文件,其中包含了端点名称等信息。
{
"endpoint": "api/willing-dingo"
}
生成了一个用于PostgreSQL连接的config.yaml文件。该文件将被后面提到的postgresql/index.graphql引用。
configurationset:
- configuration:
name: postgresql_config
uri: postgresql://postgresql.introspection.stepzen.net/introspection?user=testUserIntrospection&password=HurricaneStartingSample1934
作为顶级的GraphQL模式,index.graphql会被生成。您可以使用@sdl指令来指定其他模式文件。关于StepZen提供的指令,请参阅官方文档(GraphQL Directives Reference @rest @dbquery @graphql @materializer)中的说明。
schema @sdl(files: ["postgresql/index.graphql"]) {
query: Query
}
使用StepZen生成的GraphQL API会生成以下查询和变更操作,与PostgreSQL表对应的GraphQL模式postgresql/index.graphql已经生成。该文件使用了@materializer和@dbquery等指令。
-
- Query
id による個別の取得
List 全体の取得
PaginatedList 範囲指定による取得
ViaXXX 多対多リレーションのフィルタリング
UsingXXXfkey 外部キーによるフィルタリング
UsingXXXidx インデックス付きカラムによるフィルタリング
Mutation
insert
delete
update
然而,默认情况下,不会生成用于CRUD多对多关系表(如customeraddress或lineitem)的端点。
地址类型 {
城市: 字符串
国家地区: 字符串
客户: [顾客] @materializer(query: “customerViaCustomeraddress”)
ID: 整数!
邮政编码: 字符串
州省: 字符串
街道: 字符串
}
顾客类型 {
地址: [地址] @materializer(query: “addressViaCustomeraddress”)
电子邮件: 字符串!
ID: 整数!
名称: 字符串!
订单: [订单] @materializer(query: “orderUsingOrder_customerid_fkey”)
}
订单项类型 {
订单: 订单 @materializer(query: “orderUsingLineitem_orderid_fkey”)
订单ID: 整数!
产品: 产品 @materializer(query: “productUsingLineitem_productid_fkey”)
产品ID: 整数!
数量: 整数
}
订单类型 {
承运人: 字符串
创建日期: 日期!
顾客: 顾客 @materializer(query: “customerUsingOrder_customerid_fkey”)
顾客ID: 整数!
ID: 整数!
订单项: [订单项]
@materializer(query: “lineitemUsingLineitem_orderid_fkey”)
运费: 浮点数
追踪ID: 字符串
}
产品类型 {
描述: 字符串
ID: 整数!
图片: 字符串
订单项: [订单项] @materializer(query: “lineitem”)
标题: 字符串
}
“””
以下查询只是访问模式的一组示例。
请随意修改或聚合更多查询。
“””
查询类型 {
“查询地址类型”
address(id: 整数!): 地址
@dbquery(
type: “postgresql”
schema: “public”
table: “address”
configuration: “postgresql_config”
)
addressList: [地址]
@dbquery(
type: “postgresql”
schema: “public”
table: “address”
configuration: “postgresql_config”
)
addressPaginatedList(first: 整数, after: 整数): [地址]
@dbquery(
type: “postgresql”
schema: “public”
query: “””
SELECT “city”, “countryregion”, “id”, “postalcode”, “stateprovince”, “street” FROM “address” ORDER BY “id” LIMIT $1 OFFSET $2
“””
configuration: “postgresql_config”
)
customerViaCustomeraddress(id: 整数!): [顾客]
@dbquery(
type: “postgresql”
schema: “public”
query: “””
SELECT T.”email”, T.”id”, T.”name”
FROM “customer” T, “customeraddress” V
WHERE
V.”addressid” = $1 AND
V.”customerid” = T.”id”
“””
configuration: “postgresql_config”
)
“查询顾客类型”
addressViaCustomeraddress(id: 整数!): [地址]
@dbquery(
type: “postgresql”
schema: “public”
query: “””
SELECT T.”city”, T.”countryregion”, T.”id”, T.”postalcode”, T.”stateprovince”, T.”street”
FROM “address” T, “customeraddress” V
WHERE
V.”customerid” = $1 AND
V.”addressid” = T.”id”
“””
configuration: “postgresql_config”
)
customer(id: 整数!): 顾客
@dbquery(
type
api/postgresql/index.graphql的修改
默认情况下,lineitem是一个多对多关系表,但它没有被转换为数组,因此需要进行以下修正。
- lineitem: Lineitem @materializer(query: "lineitemUsingLineitem_orderid_fkey")
+ lineitem: [Lineitem] @materializer(query: "lineitemUsingLineitem_orderid_fkey")
- lineitemUsingLineitem_orderid_fkey(id: Int!): Lineitem
+ lineitemUsingLineitem_orderid_fkey(id: Int!): [Lineitem]
部署
用以下指令进行部署。
stepzen start
当您访问StepZen仪表板时,会显示如下屏幕,您可以尝试查询。

通过使用StepZen,我们能够从现有的数据库构建出GraphQL API。
3. 调用 GraphQL API
接下来,我们将对之前创建的React应用程序进行修改,以调用GraphQL API。
创建GraphQL查询

为了将本次所述的顾客ID作为变量传递给仪表盘左侧的查询,我们将按照以下方式修改查询。
query GetOrders($id: Int!) {
customer(id: $id) {
order {
lineitem {
product {
id
image
title
}
}
id
createdat
}
id
name
}
}
通过在仪表板画面左下角的”变量”中以 JSON 格式输入变量值,可以从仪表板执行。
{
"id": 4
}
输入后,会出现以下的画面。
同时,点击顶部的Connect按钮,可以获取用于调用GraphQL API的客户端代码。在这次中,我们将利用React Apollo的代码来实现调用的部分。

GraphQL API调用
因为我们已经创建了GraphQL查询,所以接下来要在React/Next代码中实现GraphQL API的调用部分。
这次我们将使用Apollo客户端,所以要使用npm命令进行安装。
npm install @apollo/client
为了从Apollo客户端访问GraphQL API,创建以下文件。连接信息从环境变量中读取。
// pages/_app.tsx
import { AppProps } from "next/app"; // AppProps をインポート
import { ApolloProvider } from "@apollo/client";
import { ApolloClient, InMemoryCache } from "@apollo/client";
const apiKey = process.env.NEXT_PUBLIC_API_KEY || "invalid-api-key";
const uri = process.env.NEXT_PUBLIC_API_URI || "invalid-api-uri";
const client = new ApolloClient({
uri: uri,
headers: { Authorization: `apikey ${apiKey}` },
cache: new InMemoryCache(),
});
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
);
}
export default MyApp;
环境变量的内容需要在.env文件中记录。
NEXT_PUBLIC_API_URI=https://xxx.stepzen.net/api/todos/__graphql
NEXT_PUBLIC_API_KEY=xxx::stepzen.net+1000::yyy


下一步,为了访问GraphQL API并获取购买历史信息,我们将进行以下调整。我们将使用gql来定义GraphQL查询,并使用useQuery来调用GraphQL API。最后,通过将获取的数据转换为要显示的数据,我们可以调用GraphQL API。
// pages/index.tsx
import {
AppBar,
Avatar,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Toolbar,
Typography,
} from "@mui/material";
import type { NextPage } from "next";
+ import { useQuery, gql } from "@apollo/client";
type Order = {
id: number;
createdat: string;
lineitem: Lineitem[];
};
type Lineitem = {
product: Product;
};
type Product = {
id: number;
title: string;
image: string;
};
const Home: NextPage = () => {
- const orders: Order[] = [
- {
- id: 1,
- createdat: "2023-09-25",
- lineitem: [
- {
- product: {
- id: 1,
- title: "Apple",
- image: "apple-image-url",
- },
- },
- {
- product: {
- id: 2,
- title: "Banana",
- image: "banana-image-url",
- },
- },
- ],
- },
- {
- id: 2,
- createdat: "2023-09-25",
- lineitem: [
- {
- product: {
- id: 1,
- title: "Tomato",
- image: "tomato-image-url",
- },
- },
- ],
- },
- ];
+ const { loading, error, data } = useQuery(
+ gql`
+ query GetOrders($id: Int!) {
+ customer(id: $id) {
+ order {
+ lineitem {
+ product {
+ id
+ image
+ title
+ }
+ }
+ id
+ createdat
+ }
+ id
+ name
+ }
+ }
+ `,
+ {
+ variables: { id: 4 },
+ }
+ );
+
+ if (loading) return <p>Loading...</p>;
+ if (error) return <p>Something went wrong...</p>;
+
+ const orders: Order[] = data.customer.order;
return (
<div>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Purchase History
</Typography>
</Toolbar>
</AppBar>
<div>
{orders.map((order: Order) => (
<Paper key={order.id} style={{ marginBottom: "16px" }}>
<Typography variant="h6" style={{ padding: "16px" }}>
Order ID: {order.id}
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell style={{ width: 20 }}>Image</TableCell>
<TableCell style={{ width: 1000 }}>Name</TableCell>
</TableRow>
</TableHead>
<TableBody>
{order.lineitem.map((lineitem) => (
<TableRow key={lineitem.product.id}>
<TableCell>
<Avatar
variant="square"
src={lineitem.product.image}
sx={{ width: 100, height: 100 }}
/>
</TableCell>
<TableCell>{lineitem.product.title}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
))}
</div>
</div>
);
};
export default Home;
我也将实际的代码列举如下。
// pages/index.tsx
import {
AppBar,
Avatar,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Toolbar,
Typography,
} from "@mui/material";
import type { NextPage } from "next";
import { useQuery, gql } from "@apollo/client";
type Order = {
id: number;
createdat: string;
lineitem: Lineitem[];
};
type Lineitem = {
product: Product;
};
type Product = {
id: number;
title: string;
image: string;
};
const Home: NextPage = () => {
const { loading, error, data } = useQuery(
gql`
query GetOrders($id: Int!) {
customer(id: $id) {
order {
lineitem {
product {
id
image
title
}
}
id
createdat
}
id
name
}
}
`,
{
variables: { id: 4 },
}
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Something went wrong...</p>;
const orders: Order[] = data.customer.order;
return (
<div>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Purchase History
</Typography>
</Toolbar>
</AppBar>
<div>
{orders.map((order: Order) => (
<Paper key={order.id} style={{ marginBottom: "16px" }}>
<Typography variant="h6" style={{ padding: "16px" }}>
Order ID: {order.id}
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell style={{ width: 20 }}>Image</TableCell>
<TableCell style={{ width: 1000 }}>Name</TableCell>
</TableRow>
</TableHead>
<TableBody>
{order.lineitem.map((lineitem) => (
<TableRow key={lineitem.product.id}>
<TableCell>
<Avatar
variant="square"
src={lineitem.product.image}
sx={{ width: 100, height: 100 }}
/>
</TableCell>
<TableCell>{lineitem.product.title}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
))}
</div>
</div>
);
};
export default Home;
上述的内容表示,已经成功调用并实现了 GraphQL API。
最后,通过执行以下命令并从浏览器访问localhost:3000,可以显示页面。
npm run dev

最后
通过本文章,我们介绍了如何使用StepZen将PostgreSQL数据转换为GraphQL API,并使用React和Next创建Web应用程序的方法。希望您能感受到使用GraphQL可以轻松获取数据,使用StepZen可以轻松创建GraphQL API的优势。