使用 GraphQL 和 AWS Amplify 进行深度数据建模 - 17 种数据访问模式
感谢Richard Threlkeld和Attila 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 库来支持这些数据访问模式。
本例有以下类型:
- 仓库
- 产品
- 存货
- 员工
- 客户代表
- 顾客
- 产品
让我们看一下本教程中将实现的访问模式:
- 根据员工ID查询员工详情
- 根据员工姓名查询员工详情
- 查找员工的电话号码
- 对客户的电话号码进行罚款
- 获取指定日期范围内指定客户的订单
- 显示所有客户在给定日期范围内的所有未完成订单
- 查看最近雇用的所有员工
- 查找在给定仓库工作的所有员工
- 获取给定产品的所有订单商品
- 获取所有仓库中给定产品的当前库存
- 按客户代表获取客户
- 按客户代表和日期获取订单
- 获取给定产品的所有订单商品
- 获取具有给定职位的所有员工
- 按产品和仓库获取库存
- 获取产品总库存
- 按订单总额和销售期对客户代表进行排名
以下模式介绍了所需的键和连接,以便我们可以支持 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
byName
,Employee
因为底层会创建索引,并使用查询来匹配姓名字段。我们可以使用以下查询:
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 解析器可以使用诸如Between
or 之类的谓词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 库,因此我们也将获得每种类型所需的所有基本读取和列出操作。因此,对于每种类型,我们都会有一个get
andlist
操作。
因此,对于订单、客户、员工、仓库、客户代表、库存和产品,我们也可以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