Customize your data model
Amplify automatically creates Amazon DynamoDB database tables for GraphQL types annotated with the @model
directive in your GraphQL schema. You can create relations between the data models via the @hasOne
, @hasMany
, @belongsTo
, and @manyToMany
directives.
Setup database tables
The following GraphQL schema automatically creates a database table for "Todo". @model
will also automatically add an id
field as a primary key to the database table. See Configure a primary key to learn how to customize the primary key.
type Todo @model { content: String}
Upon amplify push
or cdk deploy
, Amplify deploys the Todo database table and a corresponding GraphQL API to perform create, read, update, delete, and list operations.
In addition, @model
also adds the helper fields createdAt
and updatedAt
to your type. The values for those fields are read-only by clients unless explicitly overwritten. See Customize creation and update timestamps to learn more.
Try listing all the todos by executing the following query:
query QueryAllTodos { listTodos() { todos { items { id content createdAt updatedAt } } }}
Configure a primary key
Every GraphQL type with the @model
directive will automatically have an id
field set as the primary key. You can override this behavior by marking another required field with the @primaryKey
directive.
In the example below, todoId
is the database's primary key and an id
field will no longer be created automatically anymore by the @model
directive.
type Todo @model { todoId: ID! @primaryKey content: String}
Without any further configuration, you'll only be able to query for a Todo via an exact equality match of its primary key field. In the example above, this is the todoId
field.
Note: After a primary key is configured and deployed, you can't change it without deleting and recreating your database table.
You can also specify "sort keys" to use a combination of different fields as a primary key. This also allows you to apply more advanced sorting and filtering conditions on the specified "sort key fields".
type Inventory @model { productID: ID! @primaryKey(sortKeyFields: ["warehouseID"]) warehouseID: ID! InventoryAmount: Int!}
The schema above will allow you to pass different conditions to query the correct inventory item:
query QueryInventoryByProductAndWarehouse($productID: ID!, $warehouseID: ID!) { getInventory(productID: $productID, warehouseID: $warehouseID) { productID warehouseID inventoryAmount }}
Configure a secondary index
Amplify uses Amazon DynamoDB tables as the underlying data source for @model types. For key-value databases, it is critical to model your access patterns with "secondary indexes". Use the @index
directive to configure a secondary index.
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.
A secondary index consists of a "hash key" and, optionally, a "sort key". Use the "hash key" to perform strict equality and the "sort key" for greater than (gt), greater than or equal to (ge), less than (lt), less than or equal to (le), equals (eq), begins with, and between operations.
type Customer @model { id: ID! name: String! phoneNumber: String accountRepresentativeID: ID! @index}
The example client query below allows you to query for "Customer" records based on their accountRepresentativeID
:
query QueryCustomersForAccountRepresentative($accountRepresentativeID: ID!) { customersByAccountRepresentativeID( accountRepresentativeID: $accountRepresentativeID ) { customers { items { id name phoneNumber } } }}
You can also overwrite the queryField
or name
to customize the GraphQL query name, or secondary index name respectively:
type Customer @model { id: ID! name: String! phoneNumber: String accountRepresentativeID: ID! @index(name: "byRepresentative", queryField: "customerByRepresentative")}
query QueryCustomersForAccountRepresentative($representativeId: ID!) { customerByRepresentative(accountRepresentativeID: $representativeId) { customers { items { id name phoneNumber } } }}
To optionally configure sort keys, provide the additional fields in the sortKeyFields
parameter:
type Customer @model @auth(rules: [{ allow: public }]) { id: ID! name: String! @index(name: "byNameAndPhoneNumber", sortKeyFields: ["phoneNumber"], queryField: "customerByNameAndPhone") phoneNumber: String accountRepresentativeID: ID! @index
The example client query below allows you to query for "Customer" based on their name
and filter based on phoneNumber
:
query MyQuery { customerByNameAndPhone(phoneNumber: { beginsWith: "+1" }, name: "Rene") { items { id name phoneNumber } }}
Advanced
Rename generated queries, mutations, and subscriptions
You can override the names of any @model
-generated GraphQL queries, mutations, and subscriptions by supplying the desired name.
type Todo @model(queries: { get: "queryFor" }) { name: String! description: String}
In the example above, you will be able to run a queryForTodo
query to get a single Todo element.
Disable generated queries, mutations, and subscriptions
You can disable specific operations by assigning their value to null
.
type Todo @model(queries: { get: null }, mutations: null, subscriptions: null) { name: String! description: String}
The example above disables the getTodo
query, all mutations, and all subscriptions while allowing the generation of other queries such as listTodo
.
Creating a custom query
You can disable the get
query and create a custom query that enables us to retrieve a single Todo model.
type Query { getMyTodo(id: ID!): Todo @function(name: "getmytodofunction-${env}")}
The example above creates a custom query that utilizes the @function
directive to call a Lambda function for this query.
For the type definitions of queries, mutations, and subscriptions, see Type Definitions of the @model
Directive.
Customize creation and update timestamps
The @model
directive automatically adds createdAt
and updatedAt
timestamps to each entity. The timestamp field names can be changed by passing timestamps attribute to the directive.
type Todo @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) { name: String! description: String}
For example, the schema above will allow you to query for the following contents:
type Todo { id: ID! name: String! description: String createdOn: AWSDateTime! updatedOn: AWSDateTime!}
Modify subscriptions (real-time updates) access level
By default, real-time updates are on for all @model
types, which means customers receive real-time updates and authorization rules are applied during initial connection time. You can also turn off subscriptions for that model or make the real-time updates public, receivable by all subscribers.
type Todo @model(subscriptions: { level: off }) { # or level: public name: String! description: String}
Create multiple relationships between two models
You need to explicitly specify the connection field names if relational directives are used to create two connections of the same type between the two models.
type Individual @model { id: ID! homeAddress: Address @hasOne shippingAddress: Address @hasOne}
type Address @model { id: ID! homeIndividualID: ID shippingIndividualID: ID homeIndividual: Individual @belongsTo(fields: ["homeIndividualID"]) shipIndividual: Individual @belongsTo(fields: ["shippingIndividualID"])}
Relationships to a model with a composite primary key
When a primary key is defined by a sort key in addition to the hash key, then it's called a composite primary key.
If you explicitly define the fields
argument on the @hasOne
, @hasMany
, or @belongsTo
directives and reference a model that has a composite primary key, then you must set the values in the fields
argument in a specific order:
- The first value should always be the primary key of the related model.
- Remaining values should match the
sortKeyFields
specified in the@primaryKey
directive of the related model.
type Project @model { projectId: ID! @primaryKey(sortKeyFields: ["name"]) name: String! team: Team @hasOne(fields: ["teamId", "teamName"]) teamId: ID # customized foreign key for child primary key teamName: String # customized foreign key for child sort key}
type Team @model { teamId: ID! @primaryKey(sortKeyFields: ["name"]) name: String!}
type Project @model { projectId: ID! @primaryKey(sortKeyFields: ["name"]) name: String! team: Team @hasOne(fields: ["teamId", "teamName"]) teamId: ID # customized foreign key for child primary key teamName: String # customized foreign key for child sort key}
type Team @model { teamId: ID! @primaryKey(sortKeyFields: ["name"]) name: String! project: Project @belongsTo(fields: ["projectId", "projectName"]) projectId: ID # customized foreign key for parent primary key projectName: String # customized foreign key for parent sort key}
type Post @model { postId: ID! @primaryKey(sortKeyFields: ["title"]) title: String! comments: [Comment] @hasMany(indexName: "byPost", fields: ["postId", "title"])}
type Comment @model { commentId: ID! @primaryKey(sortKeyFields: ["content"]) content: String! postId: ID @index(name: "byPost", sortKeyFields: ["postTitle"]) # customized foreign key for parent primary key postTitle: String # customized foreign key for parent sort key}
type Post @model { postId: ID! @primaryKey(sortKeyFields: ["title"]) title: String! comments: [Comment] @hasMany(indexName: "byPost", fields: ["postId", "title"])}
type Comment @model { commentId: ID! @primaryKey(sortKeyFields: ["content"]) content: String! post: Post @belongsTo(fields: ["postId", "postTitle"]) postId: ID @index(name: "byPost", sortKeyFields: ["postTitle"]) # customized foreign key for parent primary key postTitle: String # customized foreign key for parent sort key}
Generate a secondary index without a GraphQL query
Because query creation against a secondary index is automatic, if you wish to define a secondary index that does not have a corresponding query in your API, set the queryField
parameter to null
.
type Customer @model { id: ID! name: String! phoneNumber: String accountRepresentativeID: ID! @index(queryField: null)}
Split GraphQL files
AWS Amplify supports splitting your GraphQL schema into separate .graphql
files.
You can start by creating a amplify/backend/api/<api-name>/schema/
directory. As an example, you might split up the schema for a blog site by creating Blog.graphql
, Post.graphql
, and Comment.graphql
files.
You can then run amplify api gql-compile
and the output build schema will include all the types declared across your schema files.
As your project grows, you may want to organize your custom queries, mutations, and subscriptions depending on the size and maintenance requirements of your project. You can either consolidate all of them into one file or colocate them with their corresponding models.
Using a Single Query.graphql
File
This method involves consolidating all queries into a single Query.graphql
file. It is useful for smaller projects or when you want to keep all queries in one place.
-
In the
amplify/backend/api/<api-name>/schema/
directory, create a file namedQuery.graphql
. -
Copy all query type definitions from your multiple schema files into the
Query.graphql
file. -
Make sure all your queries are properly formatted and enclosed within a single
type Query { ... }
block.
Using the extend
Keyword
Declaring a Query
type in separate schema files will result in schema validation errors similar to the following when running amplify api gql-compile
:
🛑 Schema validation failed.
There can be only one type named "Query".
Amplify GraphQL schemas support the extend
keyword, which allows you to extend types with additional fields. In this case, it also allows you to split your custom queries, mutations, and subscriptions into multiple files. This may be more ideal for larger, more complex projects.
-
Organize your GraphQL schema into multiple files as per your project's architecture.
-
In one of the files (e.g.,
schema1.graphql
), declare your type normally:
type Query { # initial custom queries}
- In other schema files (e.g.,
schema2.graphql
), use theextend
keyword to add to the type:
extend type Query { # additional custom queries}
How it works
Model directive
The @model
directive will generate:
- An Amazon DynamoDB table with PAY_PER_REQUEST billing mode enabled by default.
- An AWS AppSync DataSource configured to access the table above.
- An AWS IAM role attached to the DataSource that allows AWS AppSync to call the above table on your behalf.
- Up to 8 resolvers (create, update, delete, get, list, onCreate, onUpdate, onDelete) but this is configurable via the queries, mutations, and subscriptions arguments on the @model directive.
- Input objects for create, update, and delete mutations.
- Filter input objects that allow you to filter objects in list queries and relationship fields.
- For list queries the default number of objects returned is 100. You can override this behavior by setting the limit argument.
Type definition of the @model
directive
directive @model( queries: ModelQueryMap mutations: ModelMutationMap subscriptions: ModelSubscriptionMap timestamps: TimestampConfiguration) on OBJECT
input ModelMutationMap { create: String update: String delete: String}
input ModelQueryMap { get: String list: String}
input ModelSubscriptionMap { onCreate: [String] onUpdate: [String] onDelete: [String] level: ModelSubscriptionLevel}
enum ModelSubscriptionLevel { off public on}
input TimestampConfiguration { createdAt: String updatedAt: String}
Relational directives
The relational directives are @hasOne
, @hasMany
, @belongsTo
and @manyToMany
.
The @hasOne
will generate:
- Foreign key fields in parent type that refer to the primary key and sort key fields of the child model.
- Foreign key fields in parent input object of
create
andupdate
mutations.
Type definition of the @hasOne
directive
directive @hasOne(fields: [String!]) on FIELD_DEFINITION
The @hasMany
will generate:
- Foreign key fields in child type that refer to the primary key and sort key fields of the parent model.
- Foreign key fields in child input object of
create
andupdate
mutations. - A global secondary index (GSI) in the child type Amazon DynamoDB table.
Type definition of the @hasMany
directive
directive @hasMany( indexName: String fields: [String!] limit: Int = 100) on FIELD_DEFINITION
- The default number of nested objects returned is 100. You can override this behavior by setting the limit argument.
The @belongsTo
will generate:
- Foreign key fields that refer to the primary key and sort key fields of the related model.
- Foreign key fields in the input object of
create
andupdate
mutations.
Type definition of the @belongsTo
directive
directive @belongsTo(fields: [String!]) on FIELD_DEFINITION
The @manyToMany
will generate:
- A joint table defining the intermediate model type with the name of
relationName
. - Foreign key fields in the joint table that refer to the primary key and sort key fields of both models.
- Foreign key fields in the intermediate model input object of
create
andupdate
mutations.
Type definition of the @manyToMany
directive
directive @manyToMany( relationName: String! limit: Int = 100) on FIELD_DEFINITION
- The default number of nested objects returned is 100. You can override this behavior by setting the limit argument.