2

Modeling common GraphQL patterns in DynamoDB

 1 year ago
source link: https://advancedweb.hu/modeling-common-graphql-patterns-in-dynamodb/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Modeling common GraphQL patterns in DynamoDB

How to structure data in the tables to support efficient GraphQL queries

0fbe6c-2ddf2fa994f96c44943cffc155597d650bb408bc551d9e39009ee049348cb2f2.jpg
Tamás Sallai
3 mins

DynamoDB is all about access patterns. Since it is a NoSQL database, it does not support arbitrary queries that extract just the necessary data. DynamoDB’s queries are centered around the choice of the partition and the sort key, so depending on how you define them different data can be retrieved.

In this article, we’ll look into the most common query patterns in GraphQL and how to structure the DynamoDB tables and indices to support them.

References

This is the easiest case. An object references another object (User.group):

type User {
  id: ID!
  group: Group!
}

type Group {
  id: ID!
}

Here, store the ID of the target object in a field in the source (User.group_id):

20c5f136a73d7237bfcc629a722b2086d6934a9b6e7acd4641714906331bfc94.svg
Recommended book
d5ff54-903567c7c3141d0138f9016a9121ecf6687fe7de91f874f9711622a81d924b3b.jpg
How to design, implement, and deploy GraphQL-based APIs on the AWS cloud
Book, 2022

Then in the User.group resolver the ID is available for the GetItem operation:

{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": {
    "id": {"S": $util.toJson($ctx.source.group_id)}
  },
  "consistentRead": true
}

2-way mapping

If you need to map back to the original object, store the IDs on both ends.

For example, when you have a car and a driver:

type Driver {
  id: ID!
  car: Car
}

type Car {
  id: ID!
	driver: Driver
}
75e32086911125fdeb0a5e2428996856c21156716bed2de02d76347d8f48918a.svg

Here, the other ID is available both at the Driver.car and the Car.driver resolvers.

Lists

To map a one-to-many relationship, define an index with the partition key as the ID of the aggregate object.

For example, if the schema defines Users and Groups and the latter has a field that returns a list (Group.users):

type User {
  id: ID!
}

type Group {
  id: ID!
  users: [User!]!
}

Add an index that has the group_id as its partition key:

8054b3c0a37f12e916ec4b015eb2cad0a6614c824abe6c16595ed40fce70fc2c.svg

In the Group.users resolver, query the index with the source’s ID:

{
  "version": "2018-05-29",
  "operation": "Query",
  "index": "groupId",
  "query": {
    "expression": "#groupId = :groupId",
    "expressionNames": {
      "#groupId": "group_id"
    },
    "expressionValues": {
      ":groupId": {"S": $util.toJson($ctx.source.id)}
    }
  }
}

Pagination

The DynamoDB Query operation is paginated, which means the database might return only partial results along with a pagination token. Repeating the query with the previous token fetches the next page, and to get all items do this until there is no token in the response.

It is a best practice to expose this in the GraphQL API so that it supports arbitrary number of result items.

Related
How to store, retrieve, and query items in a DynamoDB table with an AppSync resolver

Ordering

To define the ordering of the result items, add a sort key to the table/index. This makes the position of the items deterministic, and also supports range queries on this key.

type User {
  id: ID!
  last_active: AWSDate!
}

type Group {
  id: ID!
  users(active_after: AWSDate, active_before: AWSDate, increasing: Boolean): [User!]!
}

To make this possible on the database level, add the sort key:

0ebb1a8d0f372b20fa9d4cdb64d8c5e74f12b71b5f6d0a7d8a4b353ddb5bf4d7.svg

Then in the resolver, you can define the sort key in the key condition expression and the scanIndexForward argument of the Query operation.

Filtering

The schema might define an argument field and the result should contain only matching items. For example, Users might have a have a status field and the Group.users field can accept an argument that filters the list:

type User {
  id: ID!
	status: String!
}

type Group {
  id: ID!
  users(status: String!): [User!]!
}
8f29cc6fcadc54a88a601e299e2c92196832dd677d6b635b4856e99640106cf2.svg

Filtering can be done in multiple ways in DynamoDB.

One approach is to use a filter expression. In this case, there is no need to change how you store the data, simply define the expression and DynamoDB returns only the matching items.

But using the filter expression can affect performance and lead to some surprises.

A more efficient way is to add the field to the partition key of the table or the index. This way, the value can be part of the key expression.

4dfcf5e39ef76edbff3243c87f71d8f46b6fd9f02e99eb62ad06fde9742d0447.svg

The resolver then can define both parts of the key:

{
	"version" : "2018-05-29",
	"operation" : "Query",
	"index": "groupIdStatus",
	"query": {
		"expression" : "#groupIdStatus = :groupIdStatus",
		"expressionNames": {
			"#groupIdStatus": "group_id#status"
		},
		"expressionValues" : {
			":groupIdStatus" : {"S": "$util.escapeJavaScript($ctx.source.id)#$util.escapeJavaScript($ctx.args.status)"}
		}
	},
	"limit": $util.toJson($ctx.args.count),
	"nextToken": $util.toJson($ctx.args.nextToken),
	"scanIndexForward": false
}

There are quite a few other considerations here. See this article for more info.

Related
How to structure a table to allow getting only the matching elements

Conclusion

In DynamoDB, what queries are efficient are defined by the table structure. To support the different GraphQL structures you need to adapt how you store data.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK