Page updated Jan 16, 2024

Relational models

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 {
7 id: ID!
8 title: String!
9 rating: Int!
10 status: PostStatus!
11 # New field with @hasMany
12 comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
13}
14
15# New model
16type Comment @model {
17 id: ID!
18 postID: ID! @index(name: "byPost")
19 post: Post! @belongsTo(fields: ["postID"])
20 content: String!
21}

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

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:

1Post post = Post.builder()
2 .title("My Post with comments")
3 .rating(10)
4 .status(PostStatus.ACTIVE)
5 .build();
6
7Comment comment = Comment.builder()
8 .post(post) // Directly pass in the post instance
9 .content("Loving Amplify DataStore!")
10 .build();
11
12Amplify.DataStore.save(post,
13 savedPost -> {
14 Log.i("MyAmplifyApp", "Post saved.");
15 Amplify.DataStore.save(comment,
16 savedComment -> Log.i("MyAmplifyApp", "Comment saved."),
17 failure -> Log.e("MyAmplifyApp", "Comment not saved.", failure)
18 );
19 },
20 failure -> Log.e("MyAmplifyApp", "Post not saved.", failure)
21);
1val post = Post.builder()
2 .title("My Post with comments")
3 .rating(10)
4 .status(PostStatus.ACTIVE)
5 .build()
6
7val comment = Comment.builder()
8 .post(post) // Directly pass in the post instance
9 .content("Loving Amplify DataStore!")
10 .build()
11
12Amplify.DataStore.save(post,
13 {
14 Log.i("MyAmplifyApp", "Post saved")
15 Amplify.DataStore.save(comment,
16 { Log.i("MyAmplifyApp", "Comment saved") },
17 { Log.e("MyAmplifyApp", "Comment not saved", it) }
18 )
19 },
20 { Log.e("MyAmplifyApp", "Post not saved", it) }
21)
1val post = Post.builder()
2 .title("My Post with comments")
3 .rating(10)
4 .status(PostStatus.ACTIVE)
5 .build()
6
7val comment = Comment.builder()
8 .post(post) // Directly pass in the post instance
9 .content("Loving Amplify DataStore!")
10 .build()
11
12try {
13 Amplify.DataStore.save(post)
14 Log.i("MyAmplifyApp", "Post saved.")
15
16 Amplify.DataStore.save(comment)
17 Log.i("MyAmplifyApp", "Comment saved.")
18
19} catch (error: DataStoreException) {
20 Log.e("MyAmplifyApp", "Save failed", error)
21}
1Post post = Post.builder()
2 .title("My Post with comments")
3 .rating(10)
4 .status(PostStatus.ACTIVE)
5 .build();
6
7Comment comment = Comment.builder()
8 .post(post) // Directly pass in the post instance
9 .content("Loving Amplify DataStore!")
10 .build();
11
12RxAmplify.DataStore.save(post))
13 .andThen(RxAmplify.DataStore.save(comment))
14 .subscribe(
15 () -> Log.i("MyAmplifyApp", "Saved Post and Comment."),
16 failure -> Log.e("MyAmplifyApp", "Failed to save at least one item.", failure)
17 );

Querying relations

1Amplify.DataStore.query(Comment.class, Post.STATUS.eq(PostStatus.ACTIVE),
2 matches -> {
3 while (matches.hasNext()) {
4 Comment comment = matches.next();
5 Log.i("MyAmplifyApp", "Content: " + comment.getContent());
6 }
7 },
8 failure -> Log.e("MyAmplifyApp", "Query failed.", failure)
9);
1Amplify.DataStore.query(Comment::class.java, Post.STATUS.eq(PostStatus.ACTIVE),
2 { matches ->
3 while (matches.hasNext()) {
4 val comment = matches.next()
5 Log.i("MyAmplifyApp", "Content: ${comment.content}")
6 }
7 },
8 { Log.e("MyAmplifyApp", "Query failed", it) }
9)
1Amplify.DataStore
2 .query(Comment::class, Where.matches(Post.STATUS.eq(PostStatus.ACTIVE)))
3 .catch { Log.e("MyAmplifyApp", "Query failed", it) }
4 .collect { Log.i("MyAmplifyApp", "Content: ${it.content}") }
1RxAmplify.DataStore.query(Comment.class, Post.STATUS.eq(PostStatus.ACTIVE))
2 .subscribe(
3 comment -> Log.i("MyAmplifyApp", "Content: " + comment.getContent()),
4 failure -> Log.e("MyAmplifyApp", "Query failed.", failure)
5 );

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 synced to cloud. For example, the following operation would remove the Post with id 123 as well as any related comments:

1QueryOptions queryOptions = null;
2try {
3 queryOptions = Where.identifier(Post.class, "123");
4} catch (AmplifyException e) {
5 Log.e("MyAmplifyApp", "Failed to construct QueryOptions with provided values for Where.identifier", e);
6}
7
8if (queryOptions != null) {
9 Amplify.DataStore.query(Post.class, queryOptions,
10 match -> {
11 if (match.hasNext()) {
12 Post post = match.next();
13 Amplify.DataStore.delete(post,
14 deleted -> Log.i("MyAmplifyApp", "Post deleted."),
15 failure -> Log.e("MyAmplifyApp", "Delete failed.", failure)
16 );
17 }
18 },
19 failure -> Log.e("MyAmplifyApp", "Query failed.", failure)
20 );
21}
1Amplify.DataStore.query(Post::class.java, Where.identifier(Post::class.java, "123"),
2 {
3 if (it.hasNext()) {
4 val post = it.next()
5 Amplify.DataStore.delete(post,
6 { Log.i("MyAmplifyApp", "Post deleted") },
7 { Log.e("MyAmplifyApp", "Delete failed") }
8 )
9 }
10 },
11 { Log.e("MyAmplifyApp", "Query failed", it) }
12)
1Amplify.DataStore.query(Post::class, Where.identifier(Post::class.java, "123"))
2 .catch { Log.e("MyAmplifyApp", "Query failed", it) }
3 .onEach { Amplify.DataStore.delete(it) }
4 .catch { Log.e("MyAmplifyApp", "Delete failed", it) }
5 .collect { Log.i("MyAmplifyApp", "Post deleted") }
1QueryOptions queryOptions = null;
2try {
3 queryOptions = Where.identifier(Post.class, "123");
4} catch (AmplifyException e) {
5 Log.e("MyAmplifyApp", "Failed to construct QueryOptions with provided values for Where.identifier", e);
6}
7
8if (queryOptions != null) {
9 RxAmplify.DataStore.query(Post.class, queryOptions)
10 .flatMapCompletable(RxAmplify.DataStore::delete)
11 .subscribe(
12 () -> Log.i("MyAmplifyApp", "Post deleted."),
13 failure -> Log.e("MyAmplifyApp", "Delete failed.", failure)
14 );
15}

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

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.

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}
1Post post = Post.builder()
2 .title("My First Post")
3 .build();
4
5User editor = User.builder()
6 .username("Nadia")
7 .build();
8
9PostEditor postEditor = PostEditor.builder()
10 .post(post)
11 .user(editor)
12 .build();
13
14Amplify.DataStore.save(post,
15 savedPost -> {
16 Log.i("MyAmplifyApp", "Post saved.");
17 Amplify.DataStore.save(editor,
18 savedEditor -> {
19 Log.i("MyAmplifyApp", "Editor saved.");
20 Amplify.DataStore.save(postEditor,
21 saved -> Log.i("MyAmplifyApp", "PostEditor saved."),
22 failure -> Log.e("MyAmplifyApp", "PostEditor not saved.", failure)
23 );
24 },
25 failure -> Log.e("MyAmplifyApp", "Editor not saved.", failure)
26 );
27 },
28 failure -> Log.e("MyAmplifyApp", "Post not saved.", failure)
29);
1val post = Post.builder()
2 .title("My First Post")
3 .build()
4
5val editor = User.builder()
6 .username("Nadia")
7 .build()
8
9val postEditor = PostEditor.builder()
10 .post(post)
11 .user(editor)
12 .build()
13
14Amplify.DataStore.save(post,
15 {
16 Log.i("MyAmplifyApp", "Post saved")
17 Amplify.DataStore.save(editor,
18 {
19 Log.i("MyAmplifyApp", "Editor saved")
20 Amplify.DataStore.save(postEditor,
21 { Log.i("MyAmplifyApp", "PostEditor saved") },
22 { Log.e("MyAmplifyApp", "PostEditor not saved", it) }
23 )
24 },
25 { Log.e("MyAmplifyApp", "Editor not saved", it) }
26 )
27 },
28 { Log.e("MyAmplifyApp", "Post not saved", it) }
29)
1val post = Post.builder()
2 .title("My First Post")
3 .build()
4
5val editor = User.builder()
6 .username("Nadia")
7 .build()
8
9val postEditor = PostEditor.builder()
10 .post(post)
11 .user(editor)
12 .build()
13
14try {
15 Amplify.DataStore.save(post)
16 Log.i("MyAmplifyApp", "Post saved.")
17
18 Amplify.DataStore.save(editor)
19 Log.i("MyAmplifyApp", "Editor saved.")
20
21 Amplify.DataStore.save(postEditor)
22 Log.i("MyAmplifyApp", "PostEditor saved.")
23
24} catch (error: DataStoreException) {
25 Log.e("MyAmplifyApp", "Save failed", error)
26}
1Post post = Post.builder()
2 .title("My First Post")
3 .build();
4
5User editor = User.builder()
6 .username("Nadia")
7 .build();
8
9PostEditor postEditor = PostEditor.builder()
10 .post(post)
11 .user(editor)
12 .build();
13
14Completable.mergeArray(
15 RxAmplify.DataStore.save(post),
16 RxAmplify.DataStore.save(editor)
17).andThen(
18 RxAmplify.DataStore.save(postEditor)
19).subscribe(
20 () -> Log.i("MyAmplifyApp", "Post, Editor, and PostEditor saved."),
21 failure -> Log.e("MyAmplifyApp", "Item not saved.", failure)
22);

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