Index your data with keys
@key
The @key
directive makes it simple to configure custom index structures for @model
types.
Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale but making it work for your access patterns requires a bit of forethought. DynamoDB query operations may use at most two attributes to efficiently query data. The first query argument passed to a query (the hash key) must use strict equality and the second attribute (the sort key) may use gt, ge, lt, le, eq, beginsWith, and between. DynamoDB can effectively implement a wide variety of access patterns that are powerful enough for the majority of applications.
When modeling your data during schema design there are common patterns that you may need to leverage. We provide a fully working schema with 17 patterns related to relational designs.
Definition
directive @key(fields: [String!]!, name: String, queryField: String) on OBJECT
Argument
Argument | Description |
---|---|
fields | A list of fields that should comprise the @key, used in conjunction with an @model type. The first field in the list will always be the HASH key. If two fields are provided the second field will be the SORT key. If more than two fields are provided, a single composite SORT key will be created from a combination of fields[1...n] . All generated GraphQL queries & mutations will be updated to work with custom @key directives. |
name | When provided, specifies the name of the secondary index. When omitted, specifies that the @key is defining the primary index. You may have at most one primary key per table and therefore you may have at most one @key that does not specify a name per @model type. |
queryField | When defining a secondary index (by specifying the name argument), this specifies that a new top level query field that queries the secondary index should be generated with the given name. |
How to use @key
For an introduction to the @key
directive, let's start by looking at a basic Todo
app schema with only an @model
directive.
type Todo @model { id: ID! name: String! status: String!}
By default, the @model
directive will enable the following 2 data access patterns:
getTodo
- Get a Todo byid
listTodos
- Query all Todos
You will often need additional data access patterns. For example, in a Todo app, you may want to fetch Todos by status
. The @key
directive would allow you to add this additional data access pattern with a single new line of code:
type Todo @model @key(name: "todosByStatus", fields: ["status"], queryField: "todosByStatus") { id: ID! name: String! status: String!}
Using the new todosByStatus
query you can fetch todos by status
:
query todosByStatus { todosByStatus(status: "completed") { items { id name status } }}
Next, let's take a closer look at how this works by examining a few more common data access patterns and how to model them.
Designing Data Models using @key
When designing data models using the @key
directive, the first step should be to write down your application's expected access patterns. For example, let's say you were building an e-commerce application and needed to implement access patterns like:
- Get customers by email.
- Get orders by customer by createdAt.
- Get items by order by status by createdAt.
- Get items by status by createdAt.
Let's take a look at how you would define custom keys to implement these access patterns in your schema.graphql
.
Example: Get customers by email
type Customer @model @key(fields: ["email"]) { email: String! username: String}
A @key
without a name specifies the key for the DynamoDB table's primary index. You may only provide 1 @key
without a name per @model
type.
The example above shows the simplest case where you are specifying that the table's primary index should have a simple key where the hash key is email. This allows you to get unique customers by their email.
query GetCustomerById { getCustomer(email: "me@email.com") { email username }}
This is great for simple lookup operations, but what if you need to perform slightly more complex queries?
Example: Get orders by customer email by createdAt
type Order @model @key(fields: ["customerEmail", "createdAt"]) { customerEmail: String! createdAt: AWSDateTime! orderId: ID!}
This @key
above allows you to efficiently query Order objects by both a customerEmail and the createdAt time stamp. The @key
above creates a DynamoDB table where the primary index's hash key is customerEmail and the sort key is createdAt. This allows you to write queries like this:
query ListOrdersForCustomerIn2019 { listOrders(customerEmail: "me@email.com", createdAt: { beginsWith: "2019" }) { items { orderId customerEmail createdAt } }}
The query above shows how you can use compound key structures to implement more powerful query patterns on top of DynamoDB but you are not quite done yet.
Given that DynamoDB limits you to query by at most two attributes at a time, the @key
directive helps by streamlining the process of creating composite sort keys such that you can support querying by more than two attributes at a time. For example, you can implement “Get items by orderId
, status
, and createdAt”
as well as “Get items by status
and createdAt”
for a single @model
with this schema.
type Item @model @key(fields: ["orderId", "status", "createdAt"]) @key( name: "ByStatus" fields: ["status", "createdAt"] queryField: "itemsByStatus" ) { orderId: ID! status: Status! createdAt: AWSDateTime! name: String!}enum Status { DELIVERED IN_TRANSIT PENDING UNKNOWN}
The primary @key
with 3 fields performs a bit more magic than the 1 and 2 field variants. The first field orderId will be the HASH key as expected, but the SORT key will be a new composite key named status#createdAt that is made of the status and createdAt fields on the @model. The @key
directive creates the table structures and also generates resolvers that inject composite key values for you during queries and mutations.
Using this schema, you can query the primary index to get IN_TRANSIT items created in 2019 for a given order.
# Get items for order by status by createdAt.query ListInTransitItemsForOrder { listItems( orderId: "order1" statusCreatedAt: { beginsWith: { status: IN_TRANSIT, createdAt: "2019" } } ) { items { orderId status createdAt name } }}
The query above exposes the statusCreatedAt argument that allows you to configure DynamoDB key condition expressions without worrying about how the composite key is formed under the hood. Using the same schema, you can get all PENDING items created in 2019 by querying the secondary index "ByStatus" via the Query.itemsByStatus
field.
query ItemsByStatus { itemsByStatus(status: PENDING, createdAt: { beginsWith: "2019" }) { items { orderId status createdAt name } nextToken }}
Evolving APIs with @key
There are a few important things to think about when making changes to APIs using @key
. When you need to enable a new access pattern or change an existing access pattern you should follow these steps.
- Create a new index that enables the new or updated access pattern.
- If adding an
@key
with 3 or more fields, you will need to back-fill the new composite sort key for existing data. With a@key(fields: ["email", "status", "date"])
, you would need to backfill thestatus#date
field with composite key values made up of each object's status and date fields joined by a#
. You do not need to backfill data for@key
directives with 1 or 2 fields. - Deploy your additive changes and update any downstream applications to use the new access pattern.
- Once you are certain that you do not need the old index, remove its
@key
and deploy the API again.
Deploying multiple secondary indices (GSI)
You can make multiple global secondary index (@key
with name
parameter set) updates on one "amplify push". Under the hood, Amplify CLI needs to locally sequence multiple individual deployments to your DynamoDB table because each GSI change requires time to create the new index.
Troubleshooting
If your deployment fails locally when updating multiple GSIs, you'll have the ability to run:
amplify push --iterative-rollback
to rollback the last-known-good stateamplify push --force
rollback the last-known-good state and try redeploying your changes again using.
Attempting to mutate more than 1 global secondary index at the same time.
If you're running into the error above during amplify push
, it is likely that you don't have this feature enabled. To enable multiple GSI updates, set the "enableIterativeGsiUpdates" feature flag to true
in your amplify/cli.json
.
Combining @key with @connection
Secondary indexes created with the @key
directive can be used to resolve connections when creating relationships between types. To learn how this works, check out the documentation for @connection.