使

使用 GraphQL 和 AWS Amplify 进行深度数据建模 - 17 种数据访问模式

2025-05-24

使用 GraphQL 和 AWS Amplify 进行深度数据建模 - 17 种数据访问模式

感谢Richard ThrelkeldAttila Hajdrik对撰写本文的帮助。

如何使用 GraphQL、AWS Amplify 和 NoSQL 数据库(Amazon DynamoDB)实现涵盖 17 种不同访问模式的真实且全面的数据模型。


大多数应用程序的核心只有一个:数据。能够轻松地在应用程序中建模和访问数据,并理解如何做到这一点,可以让您专注于提供核心功能和业务价值,而不是反复构建和重新构建后端。

这通常并非易事,需要深思熟虑。要做好这项工作,部分在于理解不同类型数据之间的关系以及如何让它们协同工作。

在本教程中,我将深入介绍如何使用 GraphQL、AWS Amplify、NoSQL 数据库和GraphQL Transform 库来执行此操作。


理解 GraphQL 指令

在本教程中,我们将利用@model@connection@key指令来建模数据之间的关系。为了更好地理解它的工作原理,让我们看一下这三个指令。

@model - 从基本 GraphQL 类型生成 NoSQL 数据库、解析器和 CRUD + List + 订阅 GraphQL 操作定义

@connection - 启用 GraphQL 类型之间的关系

@key - 使用底层数据库索引结构进行优化,实现带条件的高效查询

假设我们有如下所示的基本 GraphQL 类型:

type Book {
  id: ID!
  title: String
  author: Author
}

type Author {
  id: ID!
  name: String!
}

为了首先将其扩展为具有数据库、解析器、CRUD + 列表操作和订阅的完整 API,我们可以向每种类型添加@model指令:

type Book @model {
  id: ID!
  title: String
  author: Author
}

type Author @model {
  id: ID!
  name: String!
}

接下来,我们要添加书籍和作者之间的关系。为此,我们可以使用@connection指令:

type Book @model {
  id: ID!
  title: String
  author: Author @connection
}

type Author @model {
  id: ID!
  name: String!
}

接下来,假设我们想要一种按书名查询书籍的方法。该怎么做呢?我们可以Book使用@key指令来更新类型:

type Book @model
  @key(name: "byTitle", fields: ["title"], queryField: "bookByTitle") {
  id: ID!
  title: String
  author: Author @connection
}

现在,我们可以使用以下查询按书名进行查询:

query byTitle {
  bookByTitle(title: "Grapes of Wrath") {
    items {
      id
      title
    }
  }
}

@key上述示例中的指令采用以下参数

# name - name of the key
# fields - field(s) that we will be querying by
# queryField - name of the GraphQL query to be generated

@key(name: "byTitle", fields: ["title"], queryField: "bookByTitle")

让我们更进一步。如果我们想创建一个Publisher类型并将书籍分配给某个出版商,该怎么办?我们希望使用现有Book类型并将其与新Publisher类型关联。我们可以通过对架构进行以下更新来实现:

type Publisher @model {
  name: String!
  id: ID!
  books: [Book] @connection(keyName: "byPublisherId", fields: ["id"])
}

type Book @model
  @key(name: "byTitle", fields: ["title"], queryField: "bookByTitle")
  @key(name: "byPublisherId", fields: ["publisherId"], queryField: "booksByPublisherId")
{
  id: ID!
  publisherId: ID!
  title: String
  author: Author @connection
}

这里,我们添加了一个新的@key指令byPublisherId,并将其与类型的已解析books字段关联起来Publisher。现在,我们可以查询出版商,并获取相关的书籍和作者:

query listPublishers {
  listPublishers {
    items {
      id
        books {
        items {
          id
          title
          author {
            id
            name
          }
        }
      }       
    }
  }
}

此外,通过新的booksByPublisherId查询,我们还可以直接按出版商 ID 查询所有书籍:

query booksByPublisherId($publisherId: ID!) {
  booksByPublisherId(publisherId: $publisherId) {
    items {
      id
      title
    }
  }
}

了解如何使用这些指令,将为数据库的各种访问模式打开大门。在下一节中,我们将对此进行更深入的探讨。

17 访问模式

DynamoDB 文档中,关于如何在 NoSQL 数据库中建模关系数据,其中“在 DynamoDB 中建模关系数据的第一步”页面提供了一个深入的示例,其中包含 17 种访问模式。在本教程中,我将演示如何使用 GraphQL、AWS Amplify 和 GraphQL Transform 库来支持这些数据访问模式。

本例有以下类型:

  • 仓库
  • 产品
  • 存货
  • 员工
  • 客户代表
  • 顾客
  • 产品

让我们看一下本教程中将实现的访问模式:

  1. 根据员工ID查询员工详情
  2. 根据员工姓名查询员工详情
  3. 查找员工的电话号码
  4. 对客户的电话号码进行罚款
  5. 获取指定日期范围内指定客户的订单
  6. 显示所有客户在给定日期范围内的所有未完成订单
  7. 查看最近雇用的所有员工
  8. 查找在给定仓库工作的所有员工
  9. 获取给定产品的所有订单商品
  10. 获取所有仓库中给定产品的当前库存
  11. 按客户代表获取客户
  12. 按客户代表和日期获取订单
  13. 获取给定产品的所有订单商品
  14. 获取具有给定职位的所有员工
  15. 按产品和仓库获取库存
  16. 获取产品总库存
  17. 按订单总额和销售期对客户代表进行排名

以下模式介绍了所需的键和连接,以便我们可以支持 17 种访问模式。

type Order @model
  @key(name: "byCustomerByStatusByDate", fields: ["customerID", "status", "date"])
  @key(name: "byCustomerByDate", fields: ["customerID", "date"])
  @key(name: "byRepresentativebyDate", fields: ["accountRepresentativeID", "date"])
  @key(name: "byProduct", fields: ["productID", "id"])
{
  id: ID!
  customerID: ID!
  accountRepresentativeID: ID!
  productID: ID!
  status: String!
  amount: Int!
  date: String!
}

type Customer @model
  @key(name: "byRepresentative", fields: ["accountRepresentativeID", "id"])
{
  id: ID!
  name: String!
  phoneNumber: String
  accountRepresentativeID: ID!
  ordersByDate: [Order] @connection(keyName: "byCustomerByDate", fields: ["id"])
  ordersByStatusDate: [Order] @connection(keyName: "byCustomerByStatusByDate", fields: ["id"])
}

type Employee @model
  @key(name: "newHire", fields: ["newHire", "id"], queryField: "employeesNewHire")
  @key(name: "newHireByStartDate", fields: ["newHire", "startDate"], queryField: "employeesNewHireByStartDate")
  @key(name: "byName", fields: ["name", "id"], queryField: "employeeByName")
  @key(name: "byTitle", fields: ["jobTitle", "id"], queryField: "employeesByJobTitle")
  @key(name: "byWarehouse", fields: ["warehouseID", "id"])
{
  id: ID!
  name: String!
  startDate: String!
  phoneNumber: String!
  warehouseID: ID!
  jobTitle: String!
  newHire: String! # We have to use String type, because Boolean types cannot be sort keys
}

type Warehouse @model {
  id: ID!
  employees: [Employee] @connection(keyName: "byWarehouse", fields: ["id"])
}

type AccountRepresentative @model
  @key(name: "bySalesPeriodByOrderTotal", fields: ["salesPeriod", "orderTotal"], queryField: "repsByPeriodAndTotal")
{
  id: ID!
  customers: [Customer] @connection(keyName: "byRepresentative", fields: ["id"])
  orders: [Order] @connection(keyName: "byRepresentativebyDate", fields: ["id"])
  orderTotal: Int
  salesPeriod: String
}

type Inventory @model
  @key(name: "byWarehouseID", fields: ["warehouseID"], queryField: "itemsByWarehouseID")
  @key(fields: ["productID", "warehouseID"])
{
  productID: ID!
  warehouseID: ID!
  inventoryAmount: Int!
}

type Product @model {
  id: ID!
  name: String!
  orders: [Order] @connection(keyName: "byProduct", fields: ["id"])
  inventories: [Inventory] @connection(fields: ["id"])
}

现在我们已经创建了模式,让我们在数据库中创建我们将要操作的项目:

# first
mutation createWarehouse {
  createWarehouse(input: {id: "1"}) {
    id
  }
}

# second
mutation createEmployee {
  createEmployee(input: {
    id: "amanda"
    name: "Amanda",
    startDate: "2018-05-22",
    phoneNumber: "6015555555",
    warehouseID: "1",
    jobTitle: "Manager",
    newHire: "true"}
  ) {
    id
    jobTitle
    name
    newHire
    phoneNumber
    startDate
    warehouseID
  }
}

# third
mutation createAccountRepresentative {
  createAccountRepresentative(input: {
    id: "dabit"
    orderTotal: 400000
    salesPeriod: "January 2019"
  }) {
    id
    orderTotal
    salesPeriod
  }
}

# fourth
mutation createCustomer {
  createCustomer(input: {
    id: "jennifer_thomas"
    accountRepresentativeID: "dabit"
    name: "Jennifer Thomas"
    phoneNumber: "+16015555555"
  }) {
    id
    name
    accountRepresentativeID
    phoneNumber
  }
}

# fifth
mutation createProduct {
  createProduct(input: {
    id: "yeezyboost"
    name: "Yeezy Boost"
  }) {
    id
    name
  }
}

# sixth
mutation createInventory {
  createInventory(input: {
    productID: "yeezyboost"
    warehouseID: "1"
    inventoryAmount: 300
  }) {
    id
    productID
    inventoryAmount
    warehouseID
  }
}

# seventh
mutation createOrder {
  createOrder(input: {
    amount: 300
    date: "2018-07-12"
    status: "pending"
    accountRepresentativeID: "dabit"
    customerID: "jennifer_thomas"
    productID: "yeezyboost"
  }) {
    id
    customerID
    accountRepresentativeID
    amount
    date
    customerID
    productID
  }
}

1. 通过员工 ID 查找员工详细信息:
这可以通过使用员工 ID 查询员工模型来轻松完成,无需@key@connection即可完成。

query getEmployee($id: ID!) {
  getEmployee(id: $id) {
    id
    name
    phoneNumber
    startDate
    jobTitle
  }
}

2. 通过员工姓名查询员工详细信息:
类型使这种访问模式可行@key byNameEmployee因为底层会创建索引,并使用查询来匹配姓名字段。我们可以使用以下查询:

query employeeByName($name: String!) {
  employeeByName(name: $name) {
    items {
      id
      name
      phoneNumber
      startDate
      jobTitle
    }
  }
}

3. 查找员工的电话号码:
只要有员工的身份证或姓名,前面任何一个查询都可以查找员工的电话号码。

4. 查找客户的电话号码:
与上面给出的查询类似的查询,但在客户模型上,会为您提供客户的电话号码。

query getCustomer($customerID: ID!) {
  getCustomer(id: $customerID) {
    phoneNumber
  }
}

5. 获取给定日期范围内给定客户的订单:
存在一对多关系,可以查询客户的所有订单。

这种关系是通过在订单模型上设置@key名称来创建的byCustomerByDate,该名称由客户模型的订单字段上的连接进行查询。

使用日期作为排序键。这意味着 GraphQL 解析器可以使用诸如此类的谓词Between来高效地搜索日期范围,而不必扫描数据库中的所有记录然后将其过滤掉。

在某个日期范围内向客户获取订单所需的查询是:

query getCustomerWithOrdersByDate($customerID: ID!) {
  getCustomer(id: $customerID) {
    ordersByDate(date: {
      between: [ "2018-01-22", "2020-10-11" ]
    }) {
      items {
        id
        amount
        productID
      }
    }
  }
}

6. 显示所有客户在给定日期范围内的所有未结订单:
@key byCustomerByStatusByDate使您能够运行适合此访问模式的查询。

status在此示例中,使用了带有and 的复合排序键(两个或多个键的组合)date。这意味着数据库中记录的唯一标识符是通过将这两个字段(状态和日期)连接在一起创建的,然后 GraphQL 解析器可以使用诸如Betweenor 之类的谓词Contains来高效地搜索唯一标识符以查找匹配项,而无需扫描数据库中的所有记录然后将其过滤掉。

query getCustomerWithOrdersByStatusDate($customerID: ID!) {
  getCustomer(id: $customerID) {
    ordersByStatusDate (statusDate: {
      between: [
        { status: "pending" date:  "2018-01-22" },
        { status: "pending", date: "2020-10-11"}
      ]}) {
        items {
            id
            amount
            date
        }
    }
  }
}

7.查看最近雇用的所有员工:
在模型上使用“@key(name: "newHire", fields: ["newHire", "id"])”Employee可以查询员工是否最近被雇用。

query employeesNewHire {
  employeesNewHire(newHire: "true") {
    items {
      id
      name
      phoneNumber
      startDate
      jobTitle
    }
  }
}

我们还可以使用以下查询来查询并按开始日期返回结果employeesNewHireByStartDate

query employeesNewHireByDate {
  employeesNewHireByStartDate(newHire: "true") {
    items {
      id
      name
      phoneNumber
      startDate
      jobTitle
    }
  }
}

8. 查找指定仓库的所有员工:
这需要仓库与员工之间建立一对多关系。从模型中的 @connection 可以看出Warehouse,此连接使用了模型byWarehouse上的键Employee。相关查询如下所示:

query getWarehouse($warehouseID: ID!) {
  getWarehouse(id: $warehouseID) {
    id
    employees{
      items {
        id
        name
        startDate
        phoneNumber
        jobTitle
      }
    }
  }
}

9. 获取给定产品的所有订单商品:
此访问模式将使用从产品到订单的一对多关系。通过此查询,我们可以获取给定产品的所有订单:

query getProductOrders($productID: ID!) {
  getProduct(id: $productID) {
    id
    orders {
      items {
        id
        status
        amount
        date
      }
    }
  }
}

10. 获取所有仓库中某个产品的当前库存:

获取所有仓库中产品库存所需的查询如下:

query getProductInventoryInfo($productID: ID!) {
  getProduct(id: $productID) {
    id
    inventories {
      items {
        warehouseID
        inventoryAmount
      }
    }
  }
}

11.按客户代表获取客户:
这使用客户代表和客户之间的一对多连接:

所需的查询如下所示:

query getCustomersForAccountRepresentative($representativeId: ID!) {
  getAccountRepresentative(id: $representativeId) {
    customers {
      items {
        id
        name
        phoneNumber
      }
    }
  }
}

12.按客户代表和日期获取订单:

从 AccountRepresentative 模型中可以看出,此连接使用模型byRepresentativebyDate上的字段Order来创建所需的连接。所需的查询如下所示:

query getOrdersForAccountRepresentative($representativeId: ID!) {
  getAccountRepresentative(id: $representativeId) {
    id
    orders(date: {
      between: [
         "2010-01-22", "2020-10-11"
      ]
    }) {
        items {
          id
          status
          amount
          date
        }
    }
  }
}

13. 获取给定产品的所有订单商品:
这与第 9 项相同。

14. 获取具有给定职位的所有员工:
使用byTitle @key可以使这种访问模式变得非常容易。

query employeesByJobTitle {
  employeesByJobTitle(jobTitle: "Manager") {
    items {
      id
      name
      phoneNumber
      jobTitle
    }
  }
}

15. 按产品按仓库获取库存:
将库存保存在单独的模型中特别有用,因为该模型可以有自己的分区键和排序键,以便可以根据此访问模式的需要查询库存本身。

对该模型的查询如下所示:

query inventoryByProductAndWarehouse($productID: ID!, $warehouseID: ID!) {
  getInventory(productID: $productID, warehouseID: $warehouseID) {
    productID
    warehouseID
    inventoryAmount
  }
}

itemsByWarehouseID我们还可以使用由键创建的查询来获取单个仓库的所有库存byWarehouseID

query byWarehouseId($warehouseID: ID!) {
  itemsByWarehouseID(warehouseID: $warehouseID) {
    items {
      inventoryAmount
      productID
    }
  }
}

16. 获取产品总库存:
具体如何操作取决于具体用例。如果只想获取所有仓库中所有库存的列表,只需在 Inventory 模型上运行 list inventory 命令即可:

query listInventorys {
  listInventorys {
    items {
      productID
      warehouseID
      inventoryAmount
    }
  }
}

17. 按订单总额和销售周期对销售代表进行排名:
目前还不清楚这到底是什么意思。我的看法是,销售周期要么是一个日期范围,要么是一个月或一周。因此,我们可以将销售周期设置为字符串,并使用salesPeriod和 的组合进行查询orderTotal。我们也可以设置 的顺序sortDirection,以便按从大到小的顺序获取返回值:

query repsByPeriodAndTotal {
  repsByPeriodAndTotal(
    sortDirection: DESC,
    salesPeriod: "January 2019",
    orderTotal: {
      ge: 1000
    }) {
    items {
      id
      orderTotal
    }
  }
}

其他基本访问模式

由于我们使用了 GraphQL Transform 库,因此我们也将获得每种类型所需的所有基本读取和列出操作。因此,对于每种类型,我们都会有一个getandlist操作。

因此,对于订单客户员工仓库客户代表库存产品,我们也可以get通过 ID 和list操作执行基本操作:

query getOrder($id: ID!) {
  getOrder(id: $id) {
    id
    customerID
    accountRepresentativeID
    productID
    status
    amount
    date
  }
}

query listOrders {
  listOrders {
    items {
      id
      customerID
      accountRepresentativeID
      productID
      status
      amount
      date
    }
  }
}

本地运行

要在本地尝试或测试这些功能,您只需几分钟即可使用 Amplify CLI 启动并运行。

1.下载最新版本的 Amplify CLI:

$ npm install -g @aws-amplify/cli

2.配置 CLI

$ amplify configure

有关如何配置 CLI 的视频指南,请查看此处的文档

3.创建一个新的 Amplify 项目

$ amplify init

4.添加 GraphQL API

$ amplify add API

# Choose GraphQL
# Choose API key as the authorization type
# Use the schema in this tutorial

5.本地模拟 API

$ amplify mock
文章来源:https://dev.to/dabit3/data-modeling-in-depth-with-graphql-aws-amplify-17-data-access-patterns-4meh
PREV
全栈无服务器 - 使用 GraphQL、CDK、AppSync 和 React 构建实时聊天应用程序
NEXT
使用 React、Vue 和 Single-spa 构建微前端