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.
1type Todo @model {2 content: String3}
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:
1query QueryAllTodos {2 listTodos() {3 todos {4 items {5 id6 content7 createdAt8 updatedAt9 }10 }11 }12}
1import { Amplify } from 'aws-amplify';2import { generateClient } from 'aws-amplify/api';3import config from './amplifyconfiguration.json';4import { listTodos } from './graphql/queries';5
6const client = generateClient();7
8Amplify.configure(config);9
10try {11 const result = await client.graphql({ query: listTodos });12 const todos = result.data.listTodos;13} catch (res) {14 const { errors } = res;15 console.error(errors);16}
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.
1type Todo @model {2 todoId: ID! @primaryKey3 content: String4}
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".
1type Inventory @model {2 productID: ID! @primaryKey(sortKeyFields: ["warehouseID"])3 warehouseID: ID!4 InventoryAmount: Int!5}
The schema above will allow you to pass different conditions to query the correct inventory item:
1query QueryInventoryByProductAndWarehouse($productID: ID!, $warehouseID: ID!) {2 getInventory(productID: $productID, warehouseID: $warehouseID) {3 productID4 warehouseID5 inventoryAmount6 }7}
1import { getInventory } from './graphql/queries';2
3const result = await client.graphql({4 query: getInventory,5 variables: {6 productID: 'product-id',7 warehouseID: 'warehouse-id'8 }9});10const inventory = result.data.getInventory;
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.
1type Customer @model {2 id: ID!3 name: String!4 phoneNumber: String5 accountRepresentativeID: ID! @index6}
The example client query below allows you to query for "Customer" records based on their accountRepresentativeID
:
1query QueryCustomersForAccountRepresentative($accountRepresentativeID: ID!) {2 customersByAccountRepresentativeID(3 accountRepresentativeID: $accountRepresentativeID4 ) {5 customers {6 items {7 id8 name9 phoneNumber10 }11 }12 }13}
1import { customersByAccountRepresentativeID } from './graphql/queries';2
3const result = await client.graphql({4 query: customersByAccountRepresentativeID,5 variables: {6 accountRepresentativeID: 'account-rep-id'7 }8});9const customers = result.data.customersByAccountRepresentativeID;
You can also overwrite the queryField
or name
to customize the GraphQL query name, or secondary index name respectively:
1type Customer @model {2 id: ID!3 name: String!4 phoneNumber: String5 accountRepresentativeID: ID!6 @index(name: "byRepresentative", queryField: "customerByRepresentative")7}
1query QueryCustomersForAccountRepresentative($representativeId: ID!) {2 customerByRepresentative(accountRepresentativeID: $representativeId) {3 customers {4 items {5 id6 name7 phoneNumber8 }9 }10 }11}
1import { customerByRepresentative } from './graphql/queries';2
3const result = await client.graphql({4 query: customerByRepresentative,5 variables: {6 accountRepresentativeID: 'account-rep-id'7 }8});9const customer = result.data.customerByRepresentative;
To optionally configure sort keys, provide the additional fields in the sortKeyFields
parameter:
1type Customer @model @auth(rules: [{ allow: public }]) {2 id: ID!3 name: String! @index(name: "byNameAndPhoneNumber", sortKeyFields: ["phoneNumber"], queryField: "customerByNameAndPhone")4 phoneNumber: String5 accountRepresentativeID: ID! @index
The example client query below allows you to query for "Customer" based on their name
and filter based on phoneNumber
:
1query MyQuery {2 customerByNameAndPhone(phoneNumber: { beginsWith: "+1" }, name: "Rene") {3 items {4 id5 name6 phoneNumber7 }8 }9}
1import { customerByNameAndPhone } from './graphql/queries';2
3const result = await client.graphql({4 query: customerByNameAndPhone,5 variables: {6 phoneNumber: { beginsWith: '+1' },7 name: 'Rene'8 }9});10
11const customer = result.data.customerByNameAndPhone;
Setup relationships between models
Create "has one", "has many", "belongs to", and "many to many" relationships between @model
types.
Relationship | Description |
---|---|
@hasOne | Create a one-directional one-to-one relationship between two models. For example, a Project "has one" Team. This allows you to query the team from the project record. |
@hasMany | Create a one-directional one-to-many relationship between two models. For example, a Post has many comments. This allows you to query all the comments from the post record. |
@belongsTo | Use a "belongs to" relationship to make a "has one" or "has many" relationship bi-directional. For example, a Project has one Team and a Team belongs to a Project. This allows you to query the team from the project record and vice versa. |
@manyToMany | Configures a "join table" between two models to facilitate a many-to-many relationship. For example, a Blog has many Tags and a Tag has many Blogs. |
Has One relationship
Create a one-directional one-to-one relationship between two models using the @hasOne
directive.
In the example below, a Project has a Team.
1type Project @model {2 id: ID!3 name: String4 team: Team @hasOne5}6
7type Team @model {8 id: ID!9 name: String!10}
This generates queries and mutations that allow you to retrieve the related record from the source record:
1mutation CreateProject {2 createProject(input: { projectTeamId: "team-id", name: "Some Name" }) {3 team {4 name5 id6 }7 name8 id9 }10}
1import { createProject } from './graphql/mutations';2
3const result = await client.graphql({4 query: createProject,5 variables: {6 input: { projectTeamId: 'team-id', name: 'Some Name' }7 }8});9
10const project = result.data.createProject;
To customize the field to be used for storing the relationship information, set the fields
array argument and matching it to a field on the type:
1type Project @model {2 id: ID!3 name: String4 teamID: ID5 team: Team @hasOne(fields: ["teamID"])6}7
8type Team @model {9 id: ID!10 name: String!11}
In this case, the Project type has a teamID
field added as an identifier for the team. @hasOne can then get the connected Team object by querying the Team table with this teamID
:
1mutation CreateProject {2 createProject(input: { name: "New Project", teamID: "a-team-id" }) {3 id4 name5 team {6 id7 name8 }9 }10}
1import { createProject } from './graphql/mutations';2
3const result = await client.graphql({4 query: createProject,5 variables: {6 input: {7 teamID: 'team-id',8 name: 'New Project'9 }10 }11});12const project = result.data.createProject;
A @hasOne
relationship always uses a reference to the primary key of the related model, by default id
unless overridden with the @primaryKey
directive.
Has Many relationship
Create a one-directional one-to-many relationship between two models using the @hasMany
directive.
1type Post @model {2 id: ID!3 title: String!4 comments: [Comment] @hasMany5}6
7type Comment @model {8 id: ID!9 content: String!10}
This generates queries and mutations that allow you to retrieve the related Comment records from the source Post record:
1mutation CreatePost {2 createPost(input: { title: "Hello World!!" }) {3 title4 id5 comments {6 items {7 id8 content9 }10 }11 }12}
1import { createPost } from './graphql/mutations';2
3const result = await client.graphql({4 query: createPost,5 variables {6 input: { title: 'Hello World!!' },7 }8});9const post = result.data.createPost;10const comments = post.comments.items;
Under the hood, @hasMany
configures a default secondary index on the related table to enable you to query the related model from the source model.
To customize the specific secondary index used for the "has many" relationship, create an @index
directive on the field in the related table where you want to assign the secondary index.
Next, provide the secondary index with a name
attribute and a value. Optionally, you can configure a “sort key” on the secondary index by providing a sortKeyFields
attribute and a designated field as its value.
On the @hasMany
configuration, pass in the name value from your secondary index as the value for the indexName
parameter. Then, pass in the respective fields
that match the connected index.
1type Post @model {2 id: ID!3 title: String!4 comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])5}6
7type Comment @model {8 id: ID!9 postID: ID! @index(name: "byPost", sortKeyFields: ["content"])10 content: String!11}
In this case, the Comment type has a postID
field added to store the reference of Post record. The id
field referenced by @hasMany
is the id
on the Post
type. @hasMany
can then get the connected Comment object by querying the Comment table's secondary index "byPost" with this postID
:
1mutation CreatePost {2 createPost(input: { title: "Hello world!" }) {3 comments {4 items {5 postID6 content7 id8 }9 }10 title11 id12 }13}
1import { createPost, createComment } from './graphql/mutations';2import { getPost } from './graphql/mutations';3
4// create post5const result = await client.graphql({6 query: createPost,7 variables: {8 input: { title: 'Hello World!!' }9 }10});11const post = result.data.createPost;12
13// create comment14await client.graphql({15 query: createComment,16 variables: {17 input: { content: 'Hi!', postID: post.id }18 }19});20
21// get post22const result = await client.graphql({23 query: getPost,24 variables: { id: post.id }25});26
27const postWithComments = result.data.createPost;28const postComments = postWithComments.comments.items; // access comments from post
Belongs To relationship
Make a "has one" or "has many" relationship bi-directional with the @belongsTo
directive.
For 1:1 relationships, the @belongsTo directive solely facilitates the ability for you to query from both sides of the relationship. When updating a bi-directional hasOne relationship, you must update both sides of the relationship with corresponding IDs.
1type Project @model {2 id: ID!3 name: String4 team: Team @hasOne5}6
7type Team @model {8 id: ID!9 name: String!10 project: Project @belongsTo11}
This generates queries and mutations that allow you to retrieve the related Comment records from the source Post record and vice versa:
1mutation CreateProject {2 createProject(input: { name: "New Project", teamID: "a-team-id" }) {3 id4 name5 team {6 # query team from project7 id8 name9 project {10 # bi-directional query: team to project11 id12 name13 }14 }15 }16}
1import { createProject, createTeam, updateTeam } from './graphql/mutations';2
3// create team4const result = await client.graphql({5 query: createTeam,6 variables: {7 input: { name: 'New Team' }8 }9});10const team = result.data.createTeam;11
12// create project13const result = await client.graphql({14 query: createProject,15 variables: {16 input: { name: 'New Project', projectTeamId: team.id }17 }18});19const project = result.data.createProject;20const projectTeam = project.team; // access team from project21
22// update team23const updateTeamResult = await client.graphql({24 query: updateTeam,25 variables: {26 input: { id: team.id, teamProjectId: project.id }27 }28});29
30const updatedTeam = updateTeamResult.data.updateTeam;31const teamProject = updatedTeam.project; // access project from team
@belongsTo
can be used without the fields
argument. In those cases, a field is automatically generated to reference the parent’s primary key.
Alternatively, you set up a custom field to store the reference of the parent object. An example bidirectional “has many” relationship is shown below.
1type Post @model {2 id: ID!3 title: String!4 comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])5}6
7type Comment @model {8 id: ID!9 postID: ID! @index(name: "byPost", sortKeyFields: ["content"])10 content: String!11 post: Post @belongsTo(fields: ["postID"])12}
Note: The
@belongsTo
directive requires that a@hasOne
or@hasMany
relationship already exists from parent to the related model.
Many-to-many relationship
Create a many-to-many relationship between two models with the @manyToMany
directive. Provide a common relationName
on both models to join them into a many-to-many relationship.
1type Post @model {2 id: ID!3 title: String!4 content: String5 tags: [Tag] @manyToMany(relationName: "PostTags")6}7
8type Tag @model {9 id: ID!10 label: String!11 posts: [Post] @manyToMany(relationName: "PostTags")12}
Under the hood, the @manyToMany
directive will create a "join table" named after the relationName
to facilitate the many-to-many relationship. This generates queries and mutations that allow you to retrieve the related Comment records from the source Post record and vice versa:
1mutation CreatePost {2 createPost(input: { title: "Hello World!!" }) {3 id4 title5 content6 tags {7 # queries the "join table" PostTags8 items {9 tag {10 # related Tag records from Post11 id12 label13 posts {14 # queries the "join table" PostTags15 items {16 post {17 # related Post records from Tag18 id19 title20 content21 }22 }23 }24 }25 }26 }27 }28}
1import { createPost, createTag, createPostTags } from './graphql/mutations';2import { listPosts } from './graphql/queries';3
4// create post5const result = await client.graphql({6 query: createPost,7 variables: {8 input: { title: 'Hello World' }9 }10});11const post = result.data.createPost;12
13// create tag14const tagResult = await client.graphql({15 query: createTag,16 variables: {17 input: {18 label: 'My Tag'19 }20 }21});22const tag = tagResult.data.createTag;23
24// connect post and tag25await client.graphql({26 query: createPostTags,27 variables: {28 input: {29 postId: post.id,30 tagId: tag.id31 }32 }33});34
35// get posts36const listPostsResult = await client.graphql({ query: listPosts });37const posts = listPostsResult.data.listPosts;38
39const postTags = posts[0].tags; // access tags from post
Assign default values for fields
You can use the @default
directive to specify a default value for optional scalar type fields such as Int
, String
, and more.
1type Todo @model {2 content: String @default(value: "My new Todo")3}
If you create a new Todo and don't supply a content
input, Amplify will ensure that My new Todo
is auto populated as a value. When @default
is applied, non-null assertions using !
are disregarded. For example, String!
is treated the same as String
.
Server-side filtering for subscriptions
A server-side subscription filter expression is automatically generated for any @model
type.
1type Task @model {2 title: String!3 description: String4 type: String5 priority: Int6}
You can filter the subscriptions server-side by passing a filter expression. For example: If you want to subscribe to tasks of type Security
and priority greater than 5
, you can set the filter
argument accordingly.
1subscription OnCreateTask {2 onCreateTask(3 filter: { and: [{ type: { eq: "Security" } }, { priority: { gt: 5 } }] }4 ) {5 title6 description7 type8 priority9 }10}
1import { onCreateTask } from './graphql/subscriptions';2
3const subscription = client.graphql({4 query: onCreateTask,5 variables: {6 filter: {7 and: [8 { type: { eq: "Security" } }9 { priority: { gt: 5 } }10 ]11 }12 }13}).subscribe({14 next: ({ data }) => console.log(data),15 error: (error) => console.warn(error)16});
If you want to get all subscription events, don’t pass any filter
parameters.
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.
1type Todo @model(queries: { get: "queryFor" }) {2 name: String!3 description: String4}
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
.
1type Todo @model(queries: { get: null }, mutations: null, subscriptions: null) {2 name: String!3 description: String4}
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.
1type Query {2 getMyTodo(id: ID!): Todo @function(name: "getmytodofunction-${env}")3}
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.
1type Todo2 @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) {3 name: String!4 description: String5}
For example, the schema above will allow you to query for the following contents:
1type Todo {2 id: ID!3 name: String!4 description: String5 createdOn: AWSDateTime!6 updatedOn: AWSDateTime!7}
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.
1type Todo2 @model(subscriptions: { level: off }) { # or level: public3 name: String!4 description: String5}
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.
1type Individual @model {2 id: ID!3 homeAddress: Address @hasOne4 shippingAddress: Address @hasOne5}6
7type Address @model {8 id: ID!9 homeIndividualID: ID10 shippingIndividualID: ID11 homeIndividual: Individual @belongsTo(fields: ["homeIndividualID"])12 shipIndividual: Individual @belongsTo(fields: ["shippingIndividualID"])13}
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.
1type Project @model {2 projectId: ID! @primaryKey(sortKeyFields: ["name"])3 name: String!4 team: Team @hasOne(fields: ["teamId", "teamName"])5 teamId: ID # customized foreign key for child primary key6 teamName: String # customized foreign key for child sort key7}8
9type Team @model {10 teamId: ID! @primaryKey(sortKeyFields: ["name"])11 name: String!12}
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
.
1type Customer @model {2 id: ID!3 name: String!4 phoneNumber: String5 accountRepresentativeID: ID! @index(queryField: null)6}
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
1directive @model(2 queries: ModelQueryMap3 mutations: ModelMutationMap4 subscriptions: ModelSubscriptionMap5 timestamps: TimestampConfiguration6) on OBJECT7
8input ModelMutationMap {9 create: String10 update: String11 delete: String12}13
14input ModelQueryMap {15 get: String16 list: String17}18
19input ModelSubscriptionMap {20 onCreate: [String]21 onUpdate: [String]22 onDelete: [String]23 level: ModelSubscriptionLevel24}25
26enum ModelSubscriptionLevel {27 off28 public29 on30}31
32input TimestampConfiguration {33 createdAt: String34 updatedAt: String35}
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
1directive @hasOne(fields: [String!]) on FIELD_DEFINITION