diff --git a/cspell.json b/cspell.json index b7c993fba63..21b0b20eb79 100644 --- a/cspell.json +++ b/cspell.json @@ -1522,7 +1522,8 @@ "cloudwatchlogs", "userids", "xmark", - "refreshable" + "refreshable", + "generatemodelsforlazyloadandcustomselectionset" ], "flagWords": ["hte", "full-stack", "Full-stack", "Full-Stack", "sudo"], "patterns": [ diff --git a/src/directory/directory.mjs b/src/directory/directory.mjs index 573fc4bf90f..ab99c81e281 100644 --- a/src/directory/directory.mjs +++ b/src/directory/directory.mjs @@ -317,6 +317,11 @@ export const directory = { route: '/lib/graphqlapi/subscribe-data', filters: ['android', 'flutter', 'ios', 'js', 'react-native'] }, + { + title: 'Relational models', + route: '/lib/graphqlapi/relational', + filters: ['android'] + }, { title: 'Working with files and attachments', route: '/lib/graphqlapi/working-with-files', diff --git a/src/pages/lib/graphqlapi/relational/q/platform/[platform].mdx b/src/pages/lib/graphqlapi/relational/q/platform/[platform].mdx new file mode 100644 index 00000000000..2f6622d3ddf --- /dev/null +++ b/src/pages/lib/graphqlapi/relational/q/platform/[platform].mdx @@ -0,0 +1,835 @@ +export const meta = { + title: `Relational models`, + description: `Learn more about how API (GraphQL) handles relationships between Models, such as "has one", "has many", "belongs to".` +}; + +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](/cli/graphql/data-modeling). + +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](/lib/graphqlapi/getting-started#configure-api): + +```graphql +type Post @model { + id: ID! + title: String! + rating: Int! + comments: [Comment] @hasMany +} + +type Comment @model { + id: ID! + content: String + post: Post @belongsTo +} +``` + +Generate the models for the updated schema using the [Amplify CLI](/lib/graphqlapi/getting-started#generate-todo-model-class). + +```bash +amplify 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`: + + + + + + +```java +Post post = Post.builder() + .title("My Post with comments") + .rating(10) + .build(); + +Comment comment = Comment.builder() + .post(post) // Directly pass in the post instance + .content("Loving Amplify API!") + .build(); + +Amplify.API.mutate(ModelMutation.create(post), + savedPost -> { + Log.i("MyAmplifyApp", "Post created."); + Amplify.API.mutate(ModelMutation.create(comment), + savedComment -> Log.i("MyAmplifyApp", "Comment created."), + failure -> Log.e("MyAmplifyApp", "Comment not created.", failure) + ); + }, + failure -> Log.e("MyAmplifyApp", "Post not created.", failure) +); +``` + + + + +```kotlin +val post = Post.builder() + .title("My Post with comments") + .rating(10) + .build() + +val comment = Comment.builder() + .post(post) // Directly pass in the post instance + .content("Loving Amplify API!") + .build() + +Amplify.API.mutate(ModelMutation.create(post), + { + Log.i("MyAmplifyApp", "Post created") + Amplify.API.mutate(ModelMutation.create(comment), + { Log.i("MyAmplifyApp", "Comment created") }, + { Log.e("MyAmplifyApp", "Comment not created", it) } + ) + }, + { Log.e("MyAmplifyApp", "Post not created", it) } +) +``` + + + + +```kotlin +val post = Post.builder() + .title("My Post with comments") + .rating(10) + .build() + +val comment = Comment.builder() + .post(post) // Directly pass in the post instance + .content("Loving Amplify API!") + .build() + +try { + Amplify.API.mutate(ModelMutation.create(post)) + Log.i("MyAmplifyApp", "Post created.") + + Amplify.API.mutate(ModelMutation.create(comment)) + Log.i("MyAmplifyApp", "Comment created.") +} catch (error: ApiException) { + Log.e("MyAmplifyApp", "Create failed", error) +} +``` + + + + + + +## Querying relationships + +This example demonstrates an initial load of a Post with a subsequent fetch to load a page of comments for the post. + + + + + + +```java +Amplify.API.query( + ModelQuery.get(Post.class, new Post.PostIdentifier("123")), + response -> { + Post post = response.getData(); + ModelList commentsModelList = post.getComments(); + + if (commentsModelList instanceof LoadedModelList) { + List comments = + ((LoadedModelList) commentsModelList).getItems(); + Log.i("MyAmplifyApp", "Loaded " + comments.size() + " comments."); + } else if (commentsModelList instanceof LazyModelList) { + ((LazyModelList) commentsModelList).fetchPage( + page -> { + List comments = page.getItems(); + Log.i("MyAmplifyApp", "Loaded " + comments.size() + " comments."); + }, + failure -> Log.e("MyAmplifyApp, ", "Failed to fetch comments", failure) + ); + } + }, + failure -> Log.e("MyAmplifyApp", "Failed to query post.", failure) +); +``` + + + + +```kotlin +Amplify.API.query( + ModelQuery[Post::class.java, Post.PostIdentifier("123")], + { response -> + val post = response.data + when (val commentsModelList = post.comments) { + is LoadedModelList -> { + val comments = commentsModelList.items + Log.i("MyAmplifyApp", "Loaded ${comments.size} comments") + } + is LazyModelList -> { + commentsModelList.fetchPage( + { page -> + val comments = page.items + Log.i("MyAmplifyApp", "Fetched ${comments.size} comments") + }, + { Log.e("MyAmplifyApp, ", "Failed to fetch comments", it) } + ) + } + } + }, + { Log.e("MyAmplifyApp, ", "Failed to fetch post", it) } +) +``` + + + + +```kotlin +try { + val response = + Amplify.API.query(ModelQuery[Post::class.java, Post.PostIdentifier("123")]) + val post = response.data + val comments = when (val commentsModelList = post.comments) { + is LoadedModelList -> { + commentsModelList.items + } + is LazyModelList -> { + commentsModelList.fetchPage().items + } + } + Log.i("MyAmplifyApp", "Fetched ${comments.size} comments") +} catch (error: ApiException) { + Log.e("MyAmplifyApp", "Failed to fetch post and its comments", error) +} +``` + + + + +In order to handle the loaded/lazy states of relationships, the code generated models wrap relationships in `ModelReference` and `ModelList` types. + +```java +public final class Post implements Model { + + public ModelList getComments() +} + +public final class Comment implements Model { + + public ModelReference getPost() +} +``` + +ModelReference and ModelList types are either Lazy (Default) or Loaded. See [Customizing Query Depth](#customizing-query-depth-with-custom-selection-sets) to learn how to eagerly load connected relationships. + +* ModelReference + - LazyModelReference + - LoadedModelReference +* ModelList + - LazyModelList + - LoadedModelList + +### Unwrap ModelReference type + + + + +```java +void getPostFromComment(Comment comment) { + ModelReference postReference = comment.getPost(); + if (postReference instanceof LoadedModelReference) { + LoadedModelReference loadedPost = ((LoadedModelReference) postReference); + Post post = loadedPost.getValue(); + Log.i("MyAmplifyApp", "Post: " + post); + } else if (postReference instanceof LazyModelReference) { + LazyModelReference lazyPost = ((LazyModelReference) postReference); + lazyPost.fetchModel( + post -> Log.i("MyAmplifyApp", "Post: $post"), + error -> Log.e("MyAmplifyApp", "Failed to fetch post", error) + ); + } +} +``` + + + + +```kotlin +fun getPostFromComment(comment: Comment) { + when (val postReference = comment.post) { + is LoadedModelReference -> { + val post = postReference.value + Log.i("MyAmplifyApp", "Post: $post") + } + is LazyModelReference -> { + postReference.fetchModel( + { post -> Log.i("MyAmplifyApp", "Post: $post") }, + { Log.e("MyAmplifyApp", "Failed to fetch post", it) } + ) + } + } +} +``` + + + + +```kotlin +suspend fun getPostFromComment(comment: Comment) { + try { + val post = when (val postReference = comment.post) { + is LoadedModelReference -> { + postReference.value + } + + is LazyModelReference -> { + postReference.fetchModel() + } + } + Log.i("MyAmplifyApp", "Post: $post") + } catch (error: ApiException) { + Log.e("MyAmplifyApp", "Failed to fetch post", error) + } +} +``` + + + + +### Unwrap ModelList type + + + + +```java +void getCommentsForPost(Post post) { + ModelList commentsModelList = post.getComments(); + if (commentsModelList instanceof LoadedModelList) { + LoadedModelList loadedComments = ((LoadedModelList) commentsModelList); + // Eager loading loads the 1st page only. + loadedComments.getItems(); + } else if (commentsModelList instanceof LazyModelList) { + LazyModelList lazyComments = ((LazyModelList) commentsModelList); + fetchComments(lazyComments, null); + } +} + +void fetchComments(LazyModelList lazyComments, PaginationToken token) { + lazyComments.fetchPage( + token, + page -> { + List comments = page.getItems(); + Log.i("MyAmplifyApp", "Page of comments: " + comments); + if (page.getHasNextPage()) { + PaginationToken nextToken = page.getNextToken(); + fetchComments(lazyComments, nextToken); // recursively fetch next page + } + }, + error -> Log.e("MyAmplifyApp", "Failed to fetch comments page", error) + ); +} +``` + + + + +```kotlin +// Post comes from server response +fun getCommentsForPost(post: Post) { + when (val commentsModelList = post.comments) { + is LoadedModelList -> { + // Eager loading loads the 1st page only. + commentsModelList.items + } + is LazyModelList -> { + // Helper method to load all pages + fetchComments(commentsModelList) + } + } +} + +// Helper method for callback approach +fun fetchComments(lazyComments: LazyModelList, token: PaginationToken? = null) { + lazyComments.fetchPage( + token, + { page -> + val comments = page.items + Log.i("MyAmplifyApp", "Page of comments: $comments") + if (page.hasNextPage) { + val nextToken = page.nextToken + fetchComments(lazyComments, nextToken) // recursively fetch next page + } + }, + { Log.e("MyAmplifyApp", "Failed to fetch comments page", it) } + ) +} +``` + + + + +```kotlin +suspend fun getCommentsForPost(post: Post) { + try { + val comments = when (val commentsModelList = post.comments) { + is LoadedModelList -> { + // Eager loading loads the 1st page only. + commentsModelList.items + } + is LazyModelList -> { + var page = commentsModelList.fetchPage() + var loadedComments = mutableListOf(page.items) // initial page of comments + // loop through all pages to fetch the full list of comments + while (page.hasNextPage) { + val nextToken = page.nextToken + page = commentsModelList.fetchPage(nextToken) + // add the page of comments to the comments variable + loadedComments += page.items + } + loadedComments + } + } + Log.i("MyAmplifyApp", "Comments: $comments") + } catch (error: ApiException) { + Log.e("MyAmplifyApp", "Failed to fetch comments", error) + } +} +``` + + + + + + +## 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. + + + + + + +```java +// Delete any comments associated with parent post. +Amplify.API.mutate( + ModelMutation.delete(comment), + commentResponse -> + // Once all comments for a post are deleted, the post can be deleted. + Amplify.API.mutate( + ModelMutation.delete(post), + postResponse -> Log.i("MyAmplifyApp", "Deleted comment and post"), + (error) -> Log.e("MyAmplifyApp", "Failed to delete post", error) + ), + error -> Log.e("MyAmplifyApp", "Failed to delete comment", error) +); +``` + + + + +```kotlin +Amplify.API.mutate( + // Delete any comments associated with parent post. + ModelMutation.delete(comment), + { + // Once all comments for a post are deleted, the post can be deleted. + Amplify.API.mutate( + ModelMutation.delete(post), + { Log.i("MyAmplifyApp", "Deleted comment and post") }, + { Log.e("MyAmplifyApp", "Failed to delete post", it) } + ) + }, + { Log.e("MyAmplifyApp", "Failed to delete comment", it) } +) +``` + + + + +```kotlin +try { + // Delete any comments associated with parent post. + Amplify.API.mutate(ModelMutation.delete(comment)) + // Once all comments for a post are deleted, the post can be deleted. + Amplify.API.mutate(ModelMutation.delete(post)) + Log.i("MyAmplifyApp", "Deleted comment and post") +} catch (error: ApiException) { + Log.e("MyAmplifyApp", "Failed to delete comment and post", error) +} +``` + + + + + + +## 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 `Post`s and `Tag`s, delete the `PostTag`s join record prior to deleting a `Post` or `Tag`. + + + +```graphql +type Post @model { + id: ID! + title: String! + rating: Int + editors: [User] @manyToMany(relationName: "PostEditor") +} + +type User @model { + id: ID! + username: String! + posts: [Post] @manyToMany(relationName: "PostEditor") +} +``` + + + + + + +```java +Post post = Post.builder() + .title("My Post") + .rating(10) + .build(); + +User user = User.builder() + .username("User") + .build(); + +PostEditor postEditor = PostEditor.builder() + .post(post) + .user(user) + .build(); + +Amplify.API.mutate(ModelMutation.create(post), + createdPost -> { + Log.i("MyAmplifyApp", "Post created."); + Amplify.API.mutate(ModelMutation.create(user), + createdUser -> { + Log.i("MyAmplifyApp", "User created."); + Amplify.API.mutate(ModelMutation.create(postEditor), + createdPostEditor -> Log.i("MyAmplifyApp", "PostEditor created."), + failure -> Log.e("MyAmplifyApp", "PostEditor not created.", failure) + ); + }, + failure -> Log.e("MyAmplifyApp", "User not created.", failure) + ); + }, + failure -> Log.e("MyAmplifyApp", "Post not created.", failure) +); +``` + + + + +```kotlin +val post = Post.builder() + .title("My Post") + .rating(10) + .build() + +val user = User.builder() + .username("User") + .build() + +val postEditor = PostEditor.builder() + .post(post) + .user(user) + .build() + +Amplify.API.mutate(ModelMutation.create(post), + { + Log.i("MyAmplifyApp", "Post created") + Amplify.API.mutate(ModelMutation.create(user), + { + Log.i("MyAmplifyApp", "User created") + Amplify.API.mutate( + ModelMutation.create(postEditor), + { Log.i("MyAmplifyApp", "PostEditor created") }, + { Log.e("MyAmplifyApp", " PostEditor not created", it) } + ) + }, + { Log.e("MyAmplifyApp", " User not created", it) } + ) + }, + { Log.e("MyAmplifyApp", "Post not created", it) } +) +``` + + + + +```kotlin +val post = Post.builder() + .title("My Post") + .rating(10) + .build() + +val user = User.builder() + .username("User") + .build() + +val postEditor = PostEditor.builder() + .post(post) + .user(user) + .build() + +try { + Amplify.API.mutate(ModelMutation.create(post)) + Log.i("MyAmplifyApp", "Post created.") + + Amplify.API.mutate(ModelMutation.create(user)) + Log.i("MyAmplifyApp", "User created.") + + Amplify.API.mutate(ModelMutation.create(postEditor)) + Log.i("MyAmplifyApp", "PostEditor created.") +} catch (error: ApiException) { + Log.e("MyAmplifyApp", "Create failed", error) +} +``` + + + + + + +This example illustrates the complexity of working with multiple sequential create operations. To remove the nested callbacks, consider using Amplify's support for [Coroutines](/lib/project-setup/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: + + + + + + +```java +Amplify.API.query( + ModelQuery.get( + Comment.class, + new Comment.CommentIdentifier("c1"), + (commentPath -> includes(commentPath.getPost())) + ), + response -> { + Comment comment = response.getData(); + ModelReference postReference = comment.getPost(); + if (postReference instanceof LoadedModelReference) { + Post post = ((LoadedModelReference) postReference).getValue(); + Log.i("MyAmplifyApp", "Post: " + post); + } + }, + failure -> Log.e("MyAmplifyApp", "Failed to fetch post", failure) +); +``` + +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. + + + + +```kotlin +Amplify.API.query( + ModelQuery.get( + Comment::class.java, + Comment.CommentIdentifier("c1") + ) { commentPath -> + includes(commentPath.post) + }, + { response -> + val comment = response.data + val post = (comment.post as? LoadedModelReference)?.value + Log.i("MyAmplifyApp", "Post: $post") + }, + { Log.e("MyAmplifyApp", "Failed to fetch post", it) } +) +``` + +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. + + + + +```kotlin +try { + val comment = Amplify.API.query( + ModelQuery.get( + Comment::class.java, + Comment.CommentIdentifier("c1") + ) { commentPath -> + includes(commentPath.post) + } + ).data + val post = (comment.post as? LoadedModelReference)?.value + Log.i("MyAmplifyApp", "Post: $post") +} catch (error: ApiException) { + Log.e("MyAmplifyApp", "Failed to fetch post", error) +} +``` + +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: + + + + + + +```java +Amplify.API.query( + ModelQuery.get( + Post.class, + new Post.PostIdentifier("p1"), + (postPath -> includes(postPath.getComments())) + ), + response -> { + Post post = response.getData(); + ModelList commentsModelList = post.getComments(); + if (commentsModelList instanceof LoadedModelList) { + List comments = ((LoadedModelList) commentsModelList).getItems(); + Log.i("MyAmplifyApp", "Comments: " + comments); + } + }, + failure -> Log.e("MyAmplifyApp", "Failed to fetch post", failure) +); +``` + +The network request for post includes the comments, eagerly loading the first page of comments in a single network call. + + + + +```kotlin +Amplify.API.query( + ModelQuery.get( + Post::class.java, + Post.PostIdentifier("p1") + ) { postPath -> + includes(postPath.comments) + }, + { response -> + val post = response.data + val comments = (post.comments as? LoadedModelList)?.items + Log.i("MyAmplifyApp", "Comments: $comments") + }, + { Log.e("MyAmplifyApp", "Failed to fetch post", it) } +) +``` + +The network request for post includes the comments, eagerly loading the first page of comments in a single network call. + + + + +```kotlin +try { + val post = Amplify.API.query( + ModelQuery.get( + Post::class.java, + Post.PostIdentifier("p1") + ) { postPath -> + includes(postPath.comments) + } + ).data + val comments = (post.comments as? LoadedModelList)?.items + Log.i("MyAmplifyApp", "Comments: $comments") +} catch (error: ApiException) { + Log.e("MyAmplifyApp", "Failed to fetch post", error) +} +``` + +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. + + + + +```java +ModelQuery.get(Comment::class.java, "c1") { commentPath -> + includes(commentPath.post.comments) +} +``` +This query fetches a comment, eagerly loading the parent post and first page of comments for the post. + + + +```kotlin +ModelQuery.get( + Comment::class.java, + "c1" +) { commentPath -> + includes(commentPath.post.comments) +} +``` +This query fetches a comment, eagerly loading the parent post and first page of comments for the post. + + + + + + +```java +ModelQuery.get(PostEditor::class.java, "pe1") { postEditorPath -> + includes(postEditorPath.post, postEditorPath.comments) +} +``` +This query fetches a postEditor and eagerly loads its post and comments + + + +```kotlin +ModelQuery.get( + PostEditor::class.java, + "pe1" +) { postEditorPath -> + includes(postEditorPath.post, postEditorPath.comments) +} +``` +This query fetches a postEditor and eagerly loads its post and comments + + + +