技术栈
PostgreSQL + hasura + Apollo + GraphQL + React + Antd
适用于复杂的查询,快速开发
环境安装
安装PostgreSQL + hasura,使用docker安装
使用 Docker Compose 部署时,它会同时启动两个容器PostgreSQL 和 Hasura GraphQL ,如下
yml
version: "3.6"
services:
postgres:
image: postgres:latest
container_name: postgres
restart: always
volumes:
- ~/data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: postgrespassword
graphql-engine:
image: hasura/graphql-engine:latest
container_name: hasura
ports:
- "23333:8080"
depends_on:
- "postgres"
restart: always
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
## uncomment next line to set an admin secret
# HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
创建一个新文件夹,创建文件docker-compose.yaml复制上面内容,然后运行下面指令以安装
sh
docker-compose up -d
安装完成后使用下面指令查看正在运行的docker
sh
docker ps -a
在浏览器中输入localhost:23333/console
可以进入Hasura的控制界面,根据上面的配置,会自动连接上数据库
PostgreSQL
数据层次
- Cluster (集群)
- 集群是 PostgreSQL 实例的最高级别概念。一个集群包含多个数据库,并且所有这些数据库共享同一组配置文件、后台进程和存储区域。集群由一个特定版本的 PostgreSQL 服务器管理。
- Database (数据库)
- 每个集群可以包含多个独立的数据库。每个数据库都是一个逻辑单元,拥有自己的模式(schema)、表、索引等对象。用户连接到特定的数据库进行操作,不同数据库中的对象默认情况下是隔离的。
- Schema (模式)
- 模式是数据库内的命名空间,用于组织数据库对象如表、视图、函数等。每个数据库至少有一个名为
public
的默认模式,但你可以创建额外的模式来更好地组织你的数据和代码。模式有助于避免名称冲突,并允许你对数据库对象进行逻辑分组。
- 模式是数据库内的命名空间,用于组织数据库对象如表、视图、函数等。每个数据库至少有一个名为
- Table (表)
- 表是存储实际数据的地方。每个表都有一个唯一的名称(在同一模式内),并且由一组列定义,每列有其类型和约束。表可以包含零条或多条记录(行)。
创建实例数据库
sql
CREATE SCHEMA test;
CREATE TABLE test.users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL
);
CREATE TABLE test.orders (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES test.users(id) ON DELETE CASCADE,
product VARCHAR(100) NOT NULL,
quantity INT NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入用户
INSERT INTO test.users (name, email)
VALUES ('Alice', 'alice@example.com'),
('Bob', 'bob@example.com');
-- 插入订单
INSERT INTO test.orders (user_id, product, quantity)
VALUES (1, 'Laptop', 1),
(1, 'Mouse', 2),
(2, 'Keyboard', 1);
hasura
然后hasura会对schema的每个表建立以下的查询方法
分别是批量查询,聚合查询以及单体查询
sh
test_users
test_users_aggragate
test_users_by_pk
然后我们可以通过点击需要的数据,生成对应的graphQL查询语句,如下,然后在前端使用
GraphiQL
query MyQuery {
test_users {
email
name
id
}
}
react
创建项目
创建新项目
sh
npx create-react-app user-orders-app
cd user-orders-app
启动项目
sh
npm start
appollo
安装依赖
sh
npm install @apollo/client graphql
配置Hasura GraphQL服务器
js
// src/apollo-client.js
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:23333/v1/graphql', // 你的 Hasura GraphQL 端点
}),
cache: new InMemoryCache(),
});
export default client;
GraphiQL
编写graphql以直接操作数据库
js
// src/graphql.js
import { gql } from '@apollo/client';
// 获取所有用户
export const GET_USERS = gql`
query GetUsers {
test_users {
id
name
email
}
}
`;
// 获取指定用户的订单
export const GET_USER_ORDERS = gql`
query GetUserOrders($userId: Int!) {
test_orders(where: { user_id: { _eq: $userId } }) {
id
product
quantity
order_date
}
}
`;
// 创建用户
export const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
insert_test_users(objects: { name: $name, email: $email }) {
returning {
id
name
email
}
}
}
`;
// 删除用户
export const DELETE_USER = gql`
mutation DeleteUser($id: Int!) {
delete_test_users(where: { id: { _eq: $id } }) {
returning {
id
}
}
}
`;
// 更新用户
export const UPDATE_USER = gql`
mutation UpdateUser($id: Int!, $name: String, $email: String) {
update_test_users(where: { id: { _eq: $id } }, _set: { name: $name, email: $email }) {
returning {
id
name
email
}
}
}
`;
react
编写react前端页面
js
// src/UserOrders.js
import React, { useState } from 'react';
import { useQuery, useMutation } from '@apollo/client';
import { GET_USERS, GET_USER_ORDERS, CREATE_USER, DELETE_USER, UPDATE_USER } from './graphql';
const UserOrders = () => {
const [newName, setNewName] = useState('');
const [newEmail, setNewEmail] = useState('');
const [updateName, setUpdateName] = useState('');
const [updateEmail, setUpdateEmail] = useState('');
const [selectedUserId, setSelectedUserId] = useState(null);
// 获取用户列表
const { loading, error, data } = useQuery(GET_USERS);
// 获取指定用户的订单
const { loading: ordersLoading, data: ordersData } = useQuery(GET_USER_ORDERS, {
skip: !selectedUserId,
variables: { userId: selectedUserId },
});
// 调试信息:查看获取的数据
console.log('User Data:', data);
console.log('Orders Data:', ordersData);
// 创建用户
const [createUser] = useMutation(CREATE_USER, {
refetchQueries: [{ query: GET_USERS }],
});
// 删除用户
const [deleteUser] = useMutation(DELETE_USER, {
refetchQueries: [{ query: GET_USERS }],
});
// 更新用户
const [updateUser] = useMutation(UPDATE_USER, {
refetchQueries: [{ query: GET_USERS }],
});
const handleCreateUser = () => {
createUser({ variables: { name: newName, email: newEmail } });
setNewName('');
setNewEmail('');
};
const handleDeleteUser = (id) => {
deleteUser({ variables: { id } });
};
const handleUpdateUser = (id) => {
updateUser({
variables: { id, name: updateName, email: updateEmail },
});
setUpdateName('');
setUpdateEmail('');
};
return (
<div>
<h2>Create User</h2>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="Name"
/>
<input
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder="Email"
/>
<button onClick={handleCreateUser}>Create</button>
<h2>Users</h2>
{loading && <p>Loading users...</p>}
{error && <p>Error: {error.message}</p>}
{data && (
<ul>
{data.test_users.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
<button onClick={() => setSelectedUserId(user.id)}>
View Orders
</button>
<button onClick={() => handleDeleteUser(user.id)}>Delete</button>
<button
onClick={() => {
setUpdateName(user.name);
setUpdateEmail(user.email);
handleUpdateUser(user.id);
}}
>
Update
</button>
</li>
))}
</ul>
)}
{selectedUserId && ordersData && (
<div>
<h3>Orders for {data.test_users.find((user) => user.id === selectedUserId).name}</h3>
{ordersLoading ? (
<p>Loading orders...</p>
) : (
<ul>
{ordersData.test_orders && ordersData.test_orders.length > 0 ? (
ordersData.test_orders.map((order) => (
<li key={order.id}>
{order.product} - {order.quantity} (Ordered on {new Date(order.order_date).toLocaleString()})
</li>
))
) : (
<p>No orders found for this user.</p>
)}
</ul>
)}
</div>
)}
</div>
);
};
export default UserOrders;
然后再App.js中使用
js
// src/App.js
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import client from './apollo-client';
import UserOrders from './UserOrders';
function App() {
return (
<ApolloProvider client={client}>
<div className="App">
<h1>Users and Orders</h1>
<UserOrders />
</div>
</ApolloProvider>
);
}
export default App;
antd
ant design 蚂蚁组件库,爱来自阿里,组件库,用于美化前端页面
安装
sh
npm install antd@^4.24.2
npm install @ant-design/icons
先在index.js中引入
js
import 'antd/dist/antd.css';
然后对react页面应用样式
js
// src/UserList.js
import React, { useEffect, useState } from 'react';
import { Table, Button, Space, Modal, Form, Input, message } from 'antd';
import { useQuery, useMutation } from '@apollo/client';
import { GET_USERS, DELETE_USER, CREATE_USER, UPDATE_USER, GET_USER_ORDERS } from './graphql';
// 用户列表组件
const UserList = () => {
const { loading, error, data, refetch } = useQuery(GET_USERS);
const [deleteUser] = useMutation(DELETE_USER);
const [createUser] = useMutation(CREATE_USER);
const [updateUser] = useMutation(UPDATE_USER);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isOrdersModalVisible, setIsOrdersModalVisible] = useState(false);
const [form] = Form.useForm();
const [editingUser, setEditingUser] = useState(null);
const [selectedUser, setSelectedUser] = useState(null);
const [orders, setOrders] = useState([]);
const { data: ordersData, loading: ordersLoading, error: ordersError } = useQuery(GET_USER_ORDERS, {
variables: { userId: selectedUser?.id },
skip: !selectedUser, // 如果没有选择用户,则跳过该查询
onCompleted: (data) => setOrders(data?.test_orders || []),
});
// 显示删除用户的确认对话框
const handleDelete = async (userId) => {
try {
await deleteUser({ variables: { id: userId } });
message.success('User deleted successfully');
refetch(); // 刷新列表
} catch (err) {
message.error('Failed to delete user');
}
};
// 显示/隐藏模态框
const showModal = (user) => {
setEditingUser(user);
form.setFieldsValue(user || { name: '', email: '' });
setIsModalVisible(true);
};
const handleOk = async () => {
try {
const values = await form.validateFields();
if (editingUser) {
// 更新用户
await updateUser({
variables: { id: editingUser.id, name: values.name, email: values.email },
});
message.success('User updated successfully');
} else {
// 创建新用户
await createUser({
variables: { name: values.name, email: values.email },
});
message.success('User created successfully');
}
setIsModalVisible(false);
refetch(); // 刷新列表
} catch (err) {
message.error('Failed to save user');
}
};
const handleCancel = () => {
setIsModalVisible(false);
setIsOrdersModalVisible(false);
};
const handleUserClick = (user) => {
setSelectedUser(user);
setIsOrdersModalVisible(true);
};
const handleOrdersModalClose = () => {
setSelectedUser(null);
setIsOrdersModalVisible(false);
};
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
},
{
title: 'Actions',
key: 'actions',
render: (text, record) => (
<Space size="middle">
<Button type="link" onClick={() => showModal(record)}>Edit</Button>
<Button type="link" danger onClick={() => handleDelete(record.id)}>Delete</Button>
<Button type="link" onClick={() => handleUserClick(record)}>View Orders</Button>
</Space>
),
},
];
const orderColumns = [
{
title: 'Product',
dataIndex: 'product',
key: 'product',
},
{
title: 'Quantity',
dataIndex: 'quantity',
key: 'quantity',
},
{
title: 'Order Date',
dataIndex: 'order_date',
key: 'order_date',
render: (date) => new Date(date).toLocaleString(),
},
];
if (loading) return <div>Loading...</div>;
if (error) return <div>Error loading users</div>;
return (
<div>
<Button type="primary" onClick={() => showModal(null)} style={{ marginBottom: 16 }}>
Add User
</Button>
<Table
columns={columns}
dataSource={data.test_users}
rowKey="id"
/>
<Modal
title={editingUser ? 'Edit User' : 'Create User'}
visible={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
name="userForm"
>
<Form.Item
label="Name"
name="name"
rules={[{ required: true, message: 'Please input the name!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Email"
name="email"
rules={[{ required: true, message: 'Please input the email!' }, { type: 'email', message: 'Please input a valid email!' }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
<Modal
title={`${selectedUser?.name}'s Orders`}
visible={isOrdersModalVisible}
onCancel={handleOrdersModalClose}
footer={null}
>
{ordersLoading ? (
<div>Loading orders...</div>
) : ordersError ? (
<div>Error loading orders</div>
) : (
<Table
columns={orderColumns}
dataSource={orders}
rowKey="id"
/>
)}
</Modal>
</div>
);
};
export default UserList;