技术栈
PostgreSQL + hasura + Apollo + GraphQL + React + Antd
适用于复杂的查询,快速开发
环境安装
安装PostgreSQL + hasura,使用docker安装
使用 Docker Compose 部署时,它会同时启动两个容器PostgreSQL和 Hasura GraphQL ,如下
version: "3.6"
services:postgres:image: postgres:latestcontainer_name: postgresrestart: alwaysvolumes:- ~/data/postgres:/var/lib/postgresql/dataports:- "5432:5432"environment:POSTGRES_PASSWORD: postgrespasswordgraphql-engine:image: hasura/graphql-engine:latestcontainer_name: hasuraports:- "23333:8080"depends_on:- "postgres"restart: alwaysenvironment:HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgresHASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable consoleHASURA_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复制上面内容,然后运行下面指令以安装
docker-compose up -d
安装完成后使用下面指令查看正在运行的docker
docker ps -a
在浏览器中输入localhost:23333/console
可以进入Hasura的控制界面,根据上面的配置,会自动连接上数据库
PostgreSQL
数据层次
- Cluster (集群)
- 集群是 PostgreSQL 实例的最高级别概念。一个集群包含多个数据库,并且所有这些数据库共享同一组配置文件、后台进程和存储区域。集群由一个特定版本的 PostgreSQL 服务器管理。
- Database (数据库)
- 每个集群可以包含多个独立的数据库。每个数据库都是一个逻辑单元,拥有自己的模式(schema)、表、索引等对象。用户连接到特定的数据库进行操作,不同数据库中的对象默认情况下是隔离的。
- Schema (模式)
- 模式是数据库内的命名空间,用于组织数据库对象如表、视图、函数等。每个数据库至少有一个名为
public
的默认模式,但你可以创建额外的模式来更好地组织你的数据和代码。模式有助于避免名称冲突,并允许你对数据库对象进行逻辑分组。
- 模式是数据库内的命名空间,用于组织数据库对象如表、视图、函数等。每个数据库至少有一个名为
- Table (表)
- 表是存储实际数据的地方。每个表都有一个唯一的名称(在同一模式内),并且由一组列定义,每列有其类型和约束。表可以包含零条或多条记录(行)。
创建实例数据库
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的每个表建立以下的查询方法
分别是批量查询,聚合查询以及单体查询
test_users
test_users_aggragate
test_users_by_pk
然后我们可以通过点击需要的数据,生成对应的graphQL查询语句,如下,然后在前端使用
query MyQuery {test_users {emailnameid}
}
react
创建项目
创建新项目
npx create-react-app user-orders-app
cd user-orders-app
启动项目
npm start
appollo
安装依赖
npm install @apollo/client graphql
配置Hasura GraphQL服务器
// 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以直接操作数据库
// src/graphql.js
import { gql } from '@apollo/client';// 获取所有用户
export const GET_USERS = gql`query GetUsers {test_users {idnameemail}}
`;// 获取指定用户的订单
export const GET_USER_ORDERS = gql`query GetUserOrders($userId: Int!) {test_orders(where: { user_id: { _eq: $userId } }) {idproductquantityorder_date}}
`;// 创建用户
export const CREATE_USER = gql`mutation CreateUser($name: String!, $email: String!) {insert_test_users(objects: { name: $name, email: $email }) {returning {idnameemail}}}
`;// 删除用户
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 {idnameemail}}}
`;
react
编写react前端页面
// 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><inputtype="text"value={newName}onChange={(e) => setNewName(e.target.value)}placeholder="Name"/><inputtype="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><buttononClick={() => {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中使用
// 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 蚂蚁组件库,爱来自阿里,组件库,用于美化前端页面
安装
npm install antd@^4.24.2
npm install @ant-design/icons
先在index.js中引入
import 'antd/dist/antd.css';
然后对react页面应用样式
// 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><Tablecolumns={columns}dataSource={data.test_users}rowKey="id"/><Modaltitle={editingUser ? 'Edit User' : 'Create User'}visible={isModalVisible}onOk={handleOk}onCancel={handleCancel}confirmLoading={loading}><Formform={form}layout="vertical"name="userForm"><Form.Itemlabel="Name"name="name"rules={[{ required: true, message: 'Please input the name!' }]}><Input /></Form.Item><Form.Itemlabel="Email"name="email"rules={[{ required: true, message: 'Please input the email!' }, { type: 'email', message: 'Please input a valid email!' }]}><Input /></Form.Item></Form></Modal><Modaltitle={`${selectedUser?.name}'s Orders`}visible={isOrdersModalVisible}onCancel={handleOrdersModalClose}footer={null}>{ordersLoading ? (<div>Loading orders...</div>) : ordersError ? (<div>Error loading orders</div>) : (<Tablecolumns={orderColumns}dataSource={orders}rowKey="id"/>)}</Modal></div>);
};export default UserList;