PostgreSQL + hasura + Apollo + GraphQL + React + Antd

技术栈

PostgreSQL + hasura + Apollo + GraphQL + React + Antd

适用于复杂的查询,快速开发

环境安装

安装PostgreSQL + hasura,使用docker安装

使用 Docker Compose 部署时,它会同时启动两个容器PostgreSQLHasura 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

数据层次

  1. Cluster (集群)
    • 集群是 PostgreSQL 实例的最高级别概念。一个集群包含多个数据库,并且所有这些数据库共享同一组配置文件、后台进程和存储区域。集群由一个特定版本的 PostgreSQL 服务器管理。
  2. Database (数据库)
    • 每个集群可以包含多个独立的数据库。每个数据库都是一个逻辑单元,拥有自己的模式(schema)、表、索引等对象。用户连接到特定的数据库进行操作,不同数据库中的对象默认情况下是隔离的。
  3. Schema (模式)
    • 模式是数据库内的命名空间,用于组织数据库对象如表、视图、函数等。每个数据库至少有一个名为 public 的默认模式,但你可以创建额外的模式来更好地组织你的数据和代码。模式有助于避免名称冲突,并允许你对数据库对象进行逻辑分组。
  4. 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;
相关推荐
jnrjian1 小时前
PG 切换用户 \c -
数据库·postgresql
binbin_522 小时前
React 井字棋教程:用一个小游戏理解组件和状态
前端·react.js·前端框架
张元清3 小时前
React useObjectUrl Hook:预览文件与 Blob,不留内存泄漏(2026)
javascript·react.js
真的想不出名儿7 小时前
Md编辑器整合-react
前端·react.js·编辑器
IvorySQL20 小时前
IvorySQL Agent 探索与实践
数据库·人工智能·postgresql·oracle·ivorysql
牛仔只喝牛奶1 天前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
倒流时光三十年1 天前
PostgreSQL JSONB 操作符详解
大数据·数据库·postgresql
wear工程师1 天前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie1 天前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj501 天前
前端基础之《React(11)—state声明式》
前端·react.js