Page updated Dec 18, 2023

Relational models

API (GraphQL) has the capability to handle relationships between Models, such as has one, has many, and belongs to. In Amplify GraphQL APIs, this is done with the @hasOne, @hasMany and @belongsTo directives as defined in the GraphQL data modeling documentation.

By default, GraphQL APIs requests generate a selection set with a depth of 0. Connected relationship models are not returned in the initial request, but can be lazily loaded as needed with an additional API request. We provide mechanisms to customize the selection set, which allows connected relationships to be eagerly loaded on the initial request.

Prerequisites

The following examples have a minimum version requirement of the following:

  • Amplify CLI v12.7.0
  • Amplify Android Library v2.14.0
  • This guide uses updated model types generated by the Amplify CLI. To follow this guide, locate "generatemodelsforlazyloadandcustomselectionset" in {project-directory}/amplify/cli.json and set the value to true.

If you already have relational models in your project, you must re-run amplify codegen models after updating the feature flag. After the models have been updated, breaking changes will need to be addressed because relationship fields will now be wrapped in ModelList/ModelReference types. Follow the rest of the guide on this page information on how to use the new lazy supported models.

Create a GraphQL schema with relationships between models

For the following example, let's add a Post and Comment model to the schema:

1type Post @model {
2 id: ID!
3 title: String!
4 rating: Int!
5 comments: [Comment] @hasMany
6}
7
8type Comment @model {
9 id: ID!
10 content: String
11 post: Post @belongsTo
12}

Generate the models for the updated schema using the Amplify CLI.

1amplify codegen models

Creating relationships

In order to create connected models, you will create an instance of the model you wish to connect and pass it to Amplify.API.mutate:

1Post post = Post.builder()
2 .title("My Post with comments")
3 .rating(10)
4 .build();
5
6Comment comment = Comment.builder()
7 .post(post) // Directly pass in the post instance
8 .content("Loving Amplify API!")
9 .build();
10
11Amplify.API.mutate(ModelMutation.create(post),
12 savedPost -> {
13 Log.i("MyAmplifyApp", "Post created.");
14 Amplify.API.mutate(ModelMutation.create(comment),
15 savedComment -> Log.i("MyAmplifyApp", "Comment created."),
16 failure -> Log.e("MyAmplifyApp", "Comment not created.", failure)
17 );
18 },
19 failure -> Log.e("MyAmplifyApp", "Post not created.", failure)
20);
1val post = Post.builder()
2 .title("My Post with comments")
3 .rating(10)
4 .build()
5
6val comment = Comment.builder()
7 .post(post) // Directly pass in the post instance
8 .content("Loving Amplify API!")
9 .build()
10
11Amplify.API.mutate(ModelMutation.create(post),
12 {
13 Log.i("MyAmplifyApp", "Post created")
14 Amplify.API.mutate(ModelMutation.create(comment),
15 { Log.i("MyAmplifyApp", "Comment created") },
16 { Log.e("MyAmplifyApp", "Comment not created", it) }
17 )
18 },
19 { Log.e("MyAmplifyApp", "Post not created", it) }
20)
1val post = Post.builder()
2 .title("My Post with comments")
3 .rating(10)
4 .build()
5
6val comment = Comment.builder()
7 .post(post) // Directly pass in the post instance
8 .content("Loving Amplify API!")
9 .build()
10
11try {
12 Amplify.API.mutate(ModelMutation.create(post))
13 Log.i("MyAmplifyApp", "Post created.")
14
15 Amplify.API.mutate(ModelMutation.create(comment))
16 Log.i("MyAmplifyApp", "Comment created.")
17} catch (error: ApiException) {
18 Log.e("MyAmplifyApp", "Create failed", error)
19}

Querying relationships

This example demonstrates an initial load of a Post with a subsequent fetch to load a page of comments for the post.

1Amplify.API.query(
2 ModelQuery.get(Post.class, new Post.PostIdentifier("123")),
3 response -> {
4 Post post = response.getData();
5 ModelList<Comment> commentsModelList = post.getComments();
6
7 if (commentsModelList instanceof LoadedModelList) {
8 List<Comment> comments =
9 ((LoadedModelList<Comment>) commentsModelList).getItems();
10 Log.i("MyAmplifyApp", "Loaded " + comments.size() + " comments.");
11 } else if (commentsModelList instanceof LazyModelList) {
12 ((LazyModelList<Comment>) commentsModelList).fetchPage(
13 page -> {
14 List<Comment> comments = page.getItems();
15 Log.i("MyAmplifyApp", "Loaded " + comments.size() + " comments.");
16 },
17 failure -> Log.e("MyAmplifyApp, ", "Failed to fetch comments", failure)
18 );
19 }
20 },
21 failure -> Log.e("MyAmplifyApp", "Failed to query post.", failure)
22);
1Amplify.API.query(
2 ModelQuery[Post::class.java, Post.PostIdentifier("123")],
3 { response ->
4 val post = response.data
5 when (val commentsModelList = post.comments) {
6 is LoadedModelList -> {
7 val comments = commentsModelList.items
8 Log.i("MyAmplifyApp", "Loaded ${comments.size} comments")
9 }
10 is LazyModelList -> {
11 commentsModelList.fetchPage(
12 { page ->
13 val comments = page.items
14 Log.i("MyAmplifyApp", "Fetched ${comments.size} comments")
15 },
16 { Log.e("MyAmplifyApp, ", "Failed to fetch comments", it) }
17 )
18 }
19 }
20 },
21 { Log.e("MyAmplifyApp, ", "Failed to fetch post", it) }
22)
1try {
2 val response =
3 Amplify.API.query(ModelQuery[Post::class.java, Post.PostIdentifier("123")])
4 val post = response.data
5 val comments = when (val commentsModelList = post.comments) {
6 is LoadedModelList -> {
7 commentsModelList.items
8 }
9 is LazyModelList -> {
10 commentsModelList.fetchPage().items
11 }
12 }
13 Log.i("MyAmplifyApp", "Fetched ${comments.size} comments")
14} catch (error: ApiException) {
15 Log.e("MyAmplifyApp", "Failed to fetch post and its comments", error)
16}

In order to handle the loaded/lazy states of relationships, the code generated models wrap relationships in ModelReference and ModelList types.

1public final class Post implements Model {
2
3 public ModelList<Comment> getComments()
4}
5
6public final class Comment implements Model {
7
8 public ModelReference<Post> getPost()
9}

ModelReference and ModelList types are either Lazy (Default) or Loaded. See Customizing Query Depth to learn how to eagerly load connected relationships.

  • ModelReference<M>
    • LazyModelReference<M>
    • LoadedModelReference<M>
  • ModelList<M>
    • LazyModelList<M>
    • LoadedModelList<M>

Unwrap ModelReference type

1void getPostFromComment(Comment comment) {
2 ModelReference<Post> postReference = comment.getPost();
3 if (postReference instanceof LoadedModelReference) {
4 LoadedModelReference<Post> loadedPost = ((LoadedModelReference<Post>) postReference);
5 Post post = loadedPost.getValue();
6 Log.i("MyAmplifyApp", "Post: " + post);
7 } else if (postReference instanceof LazyModelReference) {
8 LazyModelReference<Post> lazyPost = ((LazyModelReference<Post>) postReference);
9 lazyPost.fetchModel(
10 post -> Log.i("MyAmplifyApp", "Post: $post"),
11 error -> Log.e("MyAmplifyApp", "Failed to fetch post", error)
12 );
13 }
14}
1fun getPostFromComment(comment: Comment) {
2 when (val postReference = comment.post) {
3 is LoadedModelReference -> {
4 val post = postReference.value
5 Log.i("MyAmplifyApp", "Post: $post")
6 }
7 is LazyModelReference -> {
8 postReference.fetchModel(
9 { post -> Log.i("MyAmplifyApp", "Post: $post") },
10 { Log.e("MyAmplifyApp", "Failed to fetch post", it) }
11 )
12 }
13 }
14}
1suspend fun getPostFromComment(comment: Comment) {
2 try {
3 val post = when (val postReference = comment.post) {
4 is LoadedModelReference -> {
5 postReference.value
6 }
7
8 is LazyModelReference -> {
9 postReference.fetchModel()
10 }
11 }
12 Log.i("MyAmplifyApp", "Post: $post")
13 } catch (error: ApiException) {
14 Log.e("MyAmplifyApp", "Failed to fetch post", error)
15 }
16}

Unwrap ModelList type

1void getCommentsForPost(Post post) {
2 ModelList<Comment> commentsModelList = post.getComments();
3 if (commentsModelList instanceof LoadedModelList) {
4 LoadedModelList<Comment> loadedComments = ((LoadedModelList<Comment>) commentsModelList);
5 // Eager loading loads the 1st page only.
6 loadedComments.getItems();
7 } else if (commentsModelList instanceof LazyModelList) {
8 LazyModelList<Comment> lazyComments = ((LazyModelList<Comment>) commentsModelList);
9 fetchComments(lazyComments, null);
10 }
11}
12
13void fetchComments(LazyModelList<Comment> lazyComments, PaginationToken token) {
14 lazyComments.fetchPage(
15 token,
16 page -> {
17 List<Comment> comments = page.getItems();
18 Log.i("MyAmplifyApp", "Page of comments: " + comments);
19 if (page.getHasNextPage()) {
20 PaginationToken nextToken = page.getNextToken();
21 fetchComments(lazyComments, nextToken); // recursively fetch next page
22 }
23 },
24 error -> Log.e("MyAmplifyApp", "Failed to fetch comments page", error)
25 );
26}
1// Post comes from server response
2fun getCommentsForPost(post: Post) {
3 when (val commentsModelList = post.comments) {
4 is LoadedModelList -> {
5 // Eager loading loads the 1st page only.
6 commentsModelList.items
7 }
8 is LazyModelList -> {
9 // Helper method to load all pages
10 fetchComments(commentsModelList)
11 }
12 }
13}
14
15// Helper method for callback approach
16fun fetchComments(lazyComments: LazyModelList<Comment>, token: PaginationToken? = null) {
17 lazyComments.fetchPage(
18 token,
19 { page ->
20 val comments = page.items
21 Log.i("MyAmplifyApp", "Page of comments: $comments")
22 if (page.hasNextPage) {
23 val nextToken = page.nextToken
24 fetchComments(lazyComments, nextToken) // recursively fetch next page
25 }
26 },
27 { Log.e("MyAmplifyApp", "Failed to fetch comments page", it) }
28 )
29}
1suspend fun getCommentsForPost(post: Post) {
2 try {
3 val comments = when (val commentsModelList = post.comments) {
4 is LoadedModelList -> {
5 // Eager loading loads the 1st page only.
6 commentsModelList.items
7 }
8 is LazyModelList -> {
9 var page = commentsModelList.fetchPage()
10 var loadedComments = mutableListOf(page.items) // initial page of comments
11 // loop through all pages to fetch the full list of comments
12 while (page.hasNextPage) {
13 val nextToken = page.nextToken
14 page = commentsModelList.fetchPage(nextToken)
15 // add the page of comments to the comments variable
16 loadedComments += page.items
17 }
18 loadedComments
19 }
20 }
21 Log.i("MyAmplifyApp", "Comments: $comments")
22 } catch (error: ApiException) {
23 Log.e("MyAmplifyApp", "Failed to fetch comments", error)
24 }
25}

Deleting relationships

When you delete a parent object in a one-to-many relationship, the children will not be removed. Delete the children before deleting the parent to prevent orphaned data.

1// Delete any comments associated with parent post.
2Amplify.API.mutate(
3 ModelMutation.delete(comment),
4 commentResponse ->
5 // Once all comments for a post are deleted, the post can be deleted.
6 Amplify.API.mutate(
7 ModelMutation.delete(post),
8 postResponse -> Log.i("MyAmplifyApp", "Deleted comment and post"),
9 (error) -> Log.e("MyAmplifyApp", "Failed to delete post", error)
10 ),
11 error -> Log.e("MyAmplifyApp", "Failed to delete comment", error)
12);
1Amplify.API.mutate(
2 // Delete any comments associated with parent post.
3 ModelMutation.delete(comment),
4 {
5 // Once all comments for a post are deleted, the post can be deleted.
6 Amplify.API.mutate(
7 ModelMutation.delete(post),
8 { Log.i("MyAmplifyApp", "Deleted comment and post") },
9 { Log.e("MyAmplifyApp", "Failed to delete post", it) }
10 )
11 },
12 { Log.e("MyAmplifyApp", "Failed to delete comment", it) }
13)
1try {
2 // Delete any comments associated with parent post.
3 Amplify.API.mutate(ModelMutation.delete(comment))
4 // Once all comments for a post are deleted, the post can be deleted.
5 Amplify.API.mutate(ModelMutation.delete(post))
6 Log.i("MyAmplifyApp", "Deleted comment and post")
7} catch (error: ApiException) {
8 Log.e("MyAmplifyApp", "Failed to delete comment and post", error)
9}

Many-to-many relationships

For many-to-many relationships, you can use the @manyToMany directive and specify a relationName. Under the hood, Amplify creates a join table and a one-to-many relationship from both models.

Join table records must be deleted prior to deleting the associated records. For example, for a many-to-many relationship between Posts and Tags, delete the PostTags join record prior to deleting a Post or Tag.

1type Post @model {
2 id: ID!
3 title: String!
4 rating: Int
5 editors: [User] @manyToMany(relationName: "PostEditor")
6}
7
8type User @model {
9 id: ID!
10 username: String!
11 posts: [Post] @manyToMany(relationName: "PostEditor")
12}
1Post post = Post.builder()
2 .title("My Post")
3 .rating(10)
4 .build();
5
6User user = User.builder()
7 .username("User")
8 .build();
9
10PostEditor postEditor = PostEditor.builder()
11 .post(post)
12 .user(user)
13 .build();
14
15Amplify.API.mutate(ModelMutation.create(post),
16 createdPost -> {
17 Log.i("MyAmplifyApp", "Post created.");
18 Amplify.API.mutate(ModelMutation.create(user),
19 createdUser -> {
20 Log.i("MyAmplifyApp", "User created.");
21 Amplify.API.mutate(ModelMutation.create(postEditor),
22 createdPostEditor -> Log.i("MyAmplifyApp", "PostEditor created."),
23 failure -> Log.e("MyAmplifyApp", "PostEditor not created.", failure)
24 );
25 },
26 failure -> Log.e("MyAmplifyApp", "User not created.", failure)
27 );
28 },
29 failure -> Log.e("MyAmplifyApp", "Post not created.", failure)
30);
1val post = Post.builder()
2 .title("My Post")
3 .rating(10)
4 .build()
5
6val user = User.builder()
7 .username("User")
8 .build()
9
10val postEditor = PostEditor.builder()
11 .post(post)
12 .user(user)
13 .build()
14
15Amplify.API.mutate(ModelMutation.create(post),
16 {
17 Log.i("MyAmplifyApp", "Post created")
18 Amplify.API.mutate(ModelMutation.create(user),
19 {
20 Log.i("MyAmplifyApp", "User created")
21 Amplify.API.mutate(
22 ModelMutation.create(postEditor),
23 { Log.i("MyAmplifyApp", "PostEditor created") },
24 { Log.e("MyAmplifyApp", " PostEditor not created", it) }
25 )
26 },
27 { Log.e("MyAmplifyApp", " User not created", it) }
28 )
29 },
30 { Log.e("MyAmplifyApp", "Post not created", it) }
31)
1val post = Post.builder()
2 .title("My Post")
3 .rating(10)
4 .build()
5
6val user = User.builder()
7 .username("User")
8 .build()
9
10val postEditor = PostEditor.builder()
11 .post(post)
12 .user(user)
13 .build()
14
15try {
16 Amplify.API.mutate(ModelMutation.create(post))
17 Log.i("MyAmplifyApp", "Post created.")
18
19 Amplify.API.mutate(ModelMutation.create(user))
20 Log.i("MyAmplifyApp", "User created.")
21
22 Amplify.API.mutate(ModelMutation.create(postEditor))
23 Log.i("MyAmplifyApp", "PostEditor created.")
24} catch (error: ApiException) {
25 Log.e("MyAmplifyApp", "Create failed", error)
26}

This example illustrates the complexity of working with multiple sequential create operations. To remove the nested callbacks, consider using Amplify's support for Coroutines.

Customizing query depth with custom selection sets

You can perform a nested query through one network request, by specifying which connected models to include. This is achieved by using the optional includes parameter for a GraphQL request.

Query for the Comment and the Post that it belongs to:

1Amplify.API.query(
2 ModelQuery.<Comment, CommentPath>get(
3 Comment.class,
4 new Comment.CommentIdentifier("c1"),
5 (commentPath -> includes(commentPath.getPost()))
6 ),
7 response -> {
8 Comment comment = response.getData();
9 ModelReference<Post> postReference = comment.getPost();
10 if (postReference instanceof LoadedModelReference) {
11 Post post = ((LoadedModelReference<Post>) postReference).getValue();
12 Log.i("MyAmplifyApp", "Post: " + post);
13 }
14 },
15 failure -> Log.e("MyAmplifyApp", "Failed to fetch post", failure)
16);

This will populate the selection set of the post in the GraphQL document which indicates to your GraphQL service to retrieve the post model as part of the operation. Once the comment is loaded, the post model is immediately available in-memory without requiring an additional network request.

1Amplify.API.query(
2 ModelQuery.get<Comment, CommentPath>(
3 Comment::class.java,
4 Comment.CommentIdentifier("c1")
5 ) { commentPath ->
6 includes(commentPath.post)
7 },
8 { response ->
9 val comment = response.data
10 val post = (comment.post as? LoadedModelReference)?.value
11 Log.i("MyAmplifyApp", "Post: $post")
12 },
13 { Log.e("MyAmplifyApp", "Failed to fetch post", it) }
14)

This will populate the selection set of the post in the GraphQL document which indicates to your GraphQL service to retrieve the post model as part of the operation. Once the comment is loaded, the post model is immediately available in-memory without requiring an additional network request.

1try {
2 val comment = Amplify.API.query(
3 ModelQuery.get<Comment, CommentPath>(
4 Comment::class.java,
5 Comment.CommentIdentifier("c1")
6 ) { commentPath ->
7 includes(commentPath.post)
8 }
9 ).data
10 val post = (comment.post as? LoadedModelReference)?.value
11 Log.i("MyAmplifyApp", "Post: $post")
12} catch (error: ApiException) {
13 Log.e("MyAmplifyApp", "Failed to fetch post", error)
14}

This will populate the selection set of the post in the GraphQL document which indicates to your GraphQL service to retrieve the post model as part of the operation. Once the comment is loaded, the post model is immediately available in-memory without requiring an additional network request.

Query for the Post and the first page of comments for the post:

1Amplify.API.query(
2 ModelQuery.<Post, PostPath>get(
3 Post.class,
4 new Post.PostIdentifier("p1"),
5 (postPath -> includes(postPath.getComments()))
6 ),
7 response -> {
8 Post post = response.getData();
9 ModelList<Comment> commentsModelList = post.getComments();
10 if (commentsModelList instanceof LoadedModelList) {
11 List<Comment> comments = ((LoadedModelList<Comment>) commentsModelList).getItems();
12 Log.i("MyAmplifyApp", "Comments: " + comments);
13 }
14 },
15 failure -> Log.e("MyAmplifyApp", "Failed to fetch post", failure)
16);

The network request for post includes the comments, eagerly loading the first page of comments in a single network call.

1Amplify.API.query(
2 ModelQuery.get<Post, PostPath>(
3 Post::class.java,
4 Post.PostIdentifier("p1")
5 ) { postPath ->
6 includes(postPath.comments)
7 },
8 { response ->
9 val post = response.data
10 val comments = (post.comments as? LoadedModelList)?.items
11 Log.i("MyAmplifyApp", "Comments: $comments")
12 },
13 { Log.e("MyAmplifyApp", "Failed to fetch post", it) }
14)

The network request for post includes the comments, eagerly loading the first page of comments in a single network call.

1try {
2 val post = Amplify.API.query(
3 ModelQuery.get<Post, PostPath>(
4 Post::class.java,
5 Post.PostIdentifier("p1")
6 ) { postPath ->
7 includes(postPath.comments)
8 }
9 ).data
10 val comments = (post.comments as? LoadedModelList)?.items
11 Log.i("MyAmplifyApp", "Comments: $comments")
12} catch (error: ApiException) {
13 Log.e("MyAmplifyApp", "Failed to fetch post", error)
14}

The network request for post includes the comments, eagerly loading the first page of comments in a single network call.

You can generate complex nested queries through the includes parameter.

1ModelQuery.get<Comment, CommentPath>(Comment::class.java, "c1") { commentPath ->
2 includes(commentPath.post.comments)
3}

This query fetches a comment, eagerly loading the parent post and first page of comments for the post.

1ModelQuery.get<Comment, CommentPath>(
2 Comment::class.java,
3 "c1"
4) { commentPath ->
5 includes(commentPath.post.comments)
6}

This query fetches a comment, eagerly loading the parent post and first page of comments for the post.

1ModelQuery.get<PostEditor, PostEditorPath>(PostEditor::class.java, "pe1") { postEditorPath ->
2 includes(postEditorPath.post, postEditorPath.user)
3}

This query fetches a postEditor and eagerly loads its post and user

1ModelQuery.get<PostEditor, PostEditorPath>(
2 PostEditor::class.java,
3 "pe1"
4) { postEditorPath ->
5 includes(postEditorPath.post, postEditorPath.user)
6}

This query fetches a postEditor and eagerly loads its post and user