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;
相关推荐
Rudon滨海渔村4 小时前
React-antd组件库 - 让Menu子菜单项水平排列 - 下拉菜单排序 - 自定义子菜单展示方式
前端·react.js·前端框架·react
前端熊猫4 小时前
React的状态管理库-Redux
前端·javascript·react.js
某公司摸鱼前端4 小时前
react 使用 PersistGate 白屏解决方案
前端·javascript·react.js·前端框架·reactjs
小袁搬码8 小时前
PostgreSQL17.x数据库备份命令及语法说明
数据库·postgresql·pg数据库备份
mixboot9 小时前
filecoin boost GraphQL API 查询
graphql·boost
和风中看海14 小时前
JSX:JavaScript的XML
前端·react.js
JSCON简时空16 小时前
还没下雪嘛?等不及了,自己整个 3D 雪地写写字!
前端·react.js·three.js
前端郭德纲1 天前
React useEffect使用中遇到的问题及决解方案
前端·react.js·前端框架
Domain-zhuo1 天前
如何理解React State不可变性的原则
前端·javascript·react native·react.js·前端框架·ecmascript
市民中心的蟋蟀1 天前
第六章 深入比对算法与协调器--上
前端·javascript·react.js