Page updated Jan 16, 2024

Relational models

Amplify iOS v1 is now in Maintenance Mode until May 31st, 2024. This means that we will continue to include updates to ensure compatibility with backend services and security. No new features will be introduced in v1.

Please use the latest version (v2) of Amplify Library for Swift to get started.

If you are currently using v1, follow these instructions to upgrade to v2.

Amplify libraries should be used for all new cloud connected applications. If you are currently using the AWS Mobile SDK for iOS, you can access the documentation here.

The @hasOne and @hasMany directives do not support referencing a model which then references the initial model via @hasOne or @hasMany if DataStore is enabled.

DataStore has the capability to handle relationships between Models, such as has one, has many, belongs to. In GraphQL this is done with the @hasOne, @hasMany and @index directives as defined in the GraphQL Transformer documentation.

Updated schema

For the examples below with DataStore let's add a new model to the sample schema:

1enum PostStatus {
2 ACTIVE
3 INACTIVE
4}
5
6type Post @model @auth(rules: [{allow: public}]) {
7 id: ID!
8 title: String!
9 rating: Int!
10 status: PostStatus!
11 # new field with @hasMany
12 comments: [Comment] @hasMany
13}
14
15# new model
16type Comment @model {
17 id: ID!
18 content: String
19 post: Post @belongsTo
20}

Saving relations

In order to save connected models, you will create an instance of the model you wish to connect and pass its ID to DataStore.save:

1let postWithComments = Post(title: "My post with comments",
2 rating: 5,
3 status: .active)
4
5let comment = Comment(content: "Loving Amplify DataStore", post: postWithComments)
6
7Amplify.DataStore.save(postWithComments) { postResult in
8 switch postResult {
9 case .failure(let error):
10 print("Error adding post - \(error.localizedDescription)")
11 case .success:
12 Amplify.DataStore.save(comment) { commentResult in
13 switch commentResult {
14 case .success:
15 print("Comment saved!")
16 case .failure(let error):
17 print("Error adding comment - \(error.localizedDescription)")
18 }
19 }
20 }
21}
1let postWithComments = Post(title: "My post with comments",
2 rating: 5,
3 status: .active)
4
5let comment = Comment(content: "Loving Amplify DataStore", post: postWithComments)
6
7let sink = Amplify.DataStore.save(postWithComments)
8 .flatMap { Amplify.DataStore.save(comment) }
9 .sink {
10 if case let .failure(error) = $0 {
11 print("Error adding post and comment - \(error.localizedDescription)")
12 }
13 }
14 receiveValue: {
15 print("Post and comment saved!")
16 }

Querying relations

Models with one-to-many connections are lazy-loaded when accessing the connected property, so accessing a relation is as simple as:

1Amplify.DataStore.query(Post.self, byId: "123") {
2 switch $0 {
3 case .success(let post):
4 if let postWithComments = post {
5 if let comments = postWithComments.comments {
6 for comment in comments {
7 print(comment.content)
8 }
9 }
10 } else {
11 print("Post not found")
12 }
13 case .failure(let error):
14 print("Post not found - \(error.localizedDescription)")
15 }
16}
1let sink = Amplify.DataStore.query(Post.self, byId: "123")
2 .compactMap { $0?.comments }
3 .flatMap { $0.loadAsPublisher() }
4 .sink {
5 if case let .failure(error) = $0 {
6 print("Error retrieving post \(error.localizedDescription)")
7 }
8 }
9 receiveValue: {
10 for comment in $0 {
11 print(comment.content)
12 }
13 }

The connected properties are of type List<M>, where M is the model type, and that type is a custom Swift Collection, which means that you can filter, map, etc:

1let excitedComments = postWithComments
2 .comments?
3 .compactMap { $0.content }
4 .filter { $0.contains("Wow!") }

Deleting relations

When you delete a parent object in a one-to-many relationship, the children will also be removed from the DataStore and mutations for this deletion will be sent over the network. For example, the following operation would remove the Post with id 123 as well as any related comments:

1Amplify.DataStore.query(Post.self, byId: "123") {
2 switch $0 {
3 case .success(let postWithComments):
4 // postWithComments might be nil, unwrap the optional appropriately
5 Amplify.DataStore.delete(postWithComments!) { deleteResult in
6 switch deleteResult {
7 case .success:
8 print("Post with id 123 deleted with success")
9 case .failure(let error):
10 print("Error deleting post and comments - \(error.localizedDescription)")
11 }
12 }
13 case .failure(let error):
14 print("Error fetching post with id 123 - \(error.localizedDescription)")
15 }
16}
1let sink = Amplify.DataStore.query(Post.self, byId: "123")
2 // postWithComments might be nil, unwrap the optional appropriately
3 .compactMap { $0 }
4 .flatMap { postWithComments in
5 Amplify.DataStore.delete(postWithComments)
6 }
7 .sink {
8 if case let .failure(error) = $0 {
9 print("Error deleting post and comments - \(error.localizedDescription)")
10 }
11 }
12 receiveValue: {
13 print("Post with id 123 deleted with success")
14 }

However, in a many-to-many relationship the children are not removed and you must explicitly delete them.

Many-to-many

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.

1enum PostStatus {
2 ACTIVE
3 INACTIVE
4}
5
6type Post @model {
7 id: ID!
8 title: String!
9 rating: Int
10 status: PostStatus
11 editors: [User] @manyToMany(relationName: "PostEditor")
12}
13
14type User @model {
15 id: ID!
16 username: String!
17 posts: [Post] @manyToMany(relationName: "PostEditor")
18}
1let post = Post(title: "My post with comments",
2 rating: 5,
3 status: .active)
4let editor = User(username: "Nadia")
5
6Amplify.DataStore.save(post) { postResult in
7 switch postResult {
8 case .failure(let error):
9 print("Error adding post - \(error.localizedDescription)")
10 case .success:
11 Amplify.DataStore.save(editor) { editorResult in
12 switch editorResult {
13 case .failure(let error):
14 print("Error adding user - \(error.localizedDescription)")
15 case .success:
16 let postEditor = PostEditor(post: post, editor: editor)
17 Amplify.DataStore.save(postEditor) { postEditorResult in
18 switch postEditorResult {
19 case .failure(let error):
20 print("Error saving postEditor - \(error.localizedDescription)")
21 case .success:
22 print("Saved user, post and postEditor!")
23 }
24 }
25 }
26 }
27 }
28}
1let post = Post(title: "My post with comments",
2 rating: 5,
3 status: .active)
4let editor = User(username: "Nadia")
5
6let sink = Amplify.DataStore.save(post)
7 .flatMap { _ in Amplify.DataStore.save(editor) }
8 .flatMap { _ in Amplify.DataStore.save(PostEditor(post: post, editor: editor)) }
9 .sink {
10 if case let .failure(error) = $0 {
11 print("Error saving post, user and postEditor: \(error.localizedDescription)")
12 }
13 }
14 receiveValue: { _ in
15 print("Saved user, post and postEditor!")
16 }

This example illustrates the complexity of working with multiple dependent persistence operations. The callback model is flexible but imposes some challenges when dealing with such scenarios. Prefer to use the Combine model if your app supports iOS 13 or higher. If not, the recommendation is that you use multiple functions to simplify the code.