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 v10.8.0
  • Amplify Library for Swift v2.4.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 some relationships have changed to async. 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:

1do {
2 let post = Post(title: "My post with comments",
3 rating: 10)
4 let comment = Comment(content: "Loving Amplify API!",
5 post: post) // Directly pass in the post instance
6
7 let createPostResult = try await Amplify.API.mutate(request: .create(post))
8 guard case .success = createPostResult else {
9 print("API response: \(createPostResult)")
10 return
11 }
12 print("Post created.")
13 let createCommentResult = try await Amplify.API.mutate(request: .create(comment))
14 guard case .success = createCommentResult else {
15 print("API response: \(createCommentResult)")
16 return
17 }
18 print("Comment created.")
19} catch {
20 print("Create post or comment failed", error)
21}

Querying relationships

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

1do {
2 let queryPostResult = try await Amplify.API.query(request: .get(Post.self, byIdentifier: "123"))
3 guard case .success(let queriedPostOptional) = queryPostResult,
4 let queriedPost = queriedPostOptional,
5 let comments = queriedPost.comments else {
6 print("API response: \(queryPostResult)")
7 return
8 }
9 try await comments.fetch()
10 print("Fetched \(comments.count) comments")
11} catch {
12 print("Failed to query post or fetch comments", error)
13}

Always call fetch() to load or retrieve the comments. If the comments were loaded as part of the query, it will return immediately. See Customizing Query Depth to learn how to eagerly load connected relationships.

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.

1do {
2 let deleteCommentResult = try await Amplify.API.mutate(request: .delete(comment))
3 guard case .success = deleteCommentResult else {
4 print("API response: \(deleteCommentResult)")
5 return
6 }
7 // Once all comments for a post are deleted, the post can be deleted.
8 let deletePostResult = try await Amplify.API.mutate(request: .delete(post))
9 guard case .success = deletePostResult else {
10 print("API response: \(deletePostResult)")
11 return
12 }
13 print("Deleted comment and post")
14} catch {
15 print("Failed to delete comment or post", error)
16}

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}
1do {
2 let post = Post(title: "My Post", rating: 10)
3 let user = User(username: "User")
4 let postEditor = PostEditor(post: post, user: user)
5
6 let createPostResult = try await Amplify.API.mutate(request: .create(post))
7 guard case .success = createPostResult else {
8 print("API response: \(createPostResult)")
9 return
10 }
11 let createUserResult = try await Amplify.API.mutate(request: .create(user))
12 guard case .success = createUserResult else {
13 print("API response: \(createUserResult)")
14 return
15 }
16 let createPostEditorResult = try await Amplify.API.mutate(request: .create(postEditor))
17 guard case .success = createPostEditorResult else {
18 print("API response: \(createPostEditorResult)")
19 return
20 }
21} catch {
22 print("Failed to create post, user, or post editor", error)
23}

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:

1do {
2 let queryCommentResult = try await Amplify.API.query(request:
3 .get(Comment.self,
4 byIdentifier: "c1",
5 includes: { comment in
6 [comment.post]
7 }))
8 guard case .success(let queriedCommentOptional) = queryCommentResult,
9 let queriedComment = queriedCommentOptional,
10 let loadedPost = try await queriedComment.post else {
11 print("API response: \(queryCommentResult)")
12 return
13 }
14
15 print("Post: ", loadedPost)
16} catch {
17 print("Failed to query comment with post", error)
18}

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:

1do {
2 let queryPostResult = try await Amplify.API.query(request:
3 .get(Post.self,
4 byIdentifier: "p1",
5 includes: { post in
6 [post.comments]
7 }))
8 guard case .success(let queriedPostOptional) = queryPostResult,
9 let queriedPost = queriedPostOptional,
10 let comments = queriedPost.comments else {
11 print("API response: \(queryPostResult)")
12 return
13 }
14
15 try await comments.fetch()
16 print("Comments: ", comments)
17} catch {
18 print("Failed to query post with comments", error)
19}

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.

1let queryCommentResult = try await Amplify.API.query(request:
2 .get(Comment.self,
3 byIdentifier: "p1",
4 includes: { comment in
5 [comment.post.comments]
6}))

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

1let queryCommentResult = try await Amplify.API.query(request:
2 .get(PostEditor.self,
3 byIdentifier: "pe1",
4 includes: { postEditor in
5 [postEditor.post, postEditor.user]
6}))

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