Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated Apr 29, 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:

enum PostStatus {
ACTIVE
INACTIVE
}
type Post @model @auth(rules: [{allow: public}]) {
id: ID!
title: String!
rating: Int!
status: PostStatus!
# new field with @hasMany
comments: [Comment] @hasMany
}
# new model
type Comment @model {
id: ID!
content: String
post: Post @belongsTo
}

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:

let post = Post(
title: "My post with comments",
rating: 5,
status: .active
)
let commentWithPost = Comment(
content: "Loving Amplify DataStore",
post: post
)
do {
let savedPost = try await Amplify.DataStore.save(post)
let savedCommentWithPost = try await Amplify.DataStore.save(commentWithPost)
} catch let error as DataStoreError {
print("Failed with error \(error)")
} catch {
print("Unexpected error \(error)")
}
let post = Post(
title: "My post with comments",
rating: 5,
status: .active
)
let commentWithPost = Comment(
content: "Loving Amplify DataStore",
post: post)
let sink = Amplify.Publisher.create { try await Amplify.DataStore.save(post) }
.flatMap { Amplify.Publisher.create { try await Amplify.DataStore.save(commentWithPost) } }
.sink {
if case let .failure(error) = $0 {
print("Error adding post and comment - \(error.localizedDescription)")
}
}
receiveValue: {
print("Post and comment saved!")
}

Querying relations

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

do {
guard let queriedPost = try await Amplify.DataStore.query(Post.self, byId: "123"),
let comments = queriedPost.comments else {
return
}
// call fetch to lazy load the postResult before accessing its result
try await comments.fetch()
for comment in comments {
print("\(comment)")
}
} catch let error as DataStoreError {
print("Failed to query \(error)")
} catch let error as CoreError {
print("Failed to fetch \(error)")
} catch {
print("Unexpected error \(error)")
}
let sink = Amplify.Publisher.create { try await Amplify.DataStore.query(Post.self, byId: "123") }.sink {
if case let .failure(error) = $0 {
print("Error retrieving post \(error.localizedDescription)")
}
} receiveValue: { queriedPost in
guard let queriedPost = queriedPost,
let comments = queriedPost.comments else {
return
}
// call fetch to lazy load the postResult before accessing its result
Task {
do {
try await comments.fetch()
for comment in comments {
print("\(comment)")
}
} catch let error as CoreError {
print("Failed to fetch \(error)")
} catch {
print("Unexpected error \(error)")
}
}
}

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:

let excitedComments = comments
.compactMap { $0.content }
.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 synced to cloud. For example, the following operation would remove the Post with id 123 as well as any related comments:

do {
guard let postWithComments = try await Amplify.DataStore.query(Post.self, byId: "123") else {
print("No post found")
return
}
try await Amplify.DataStore.delete(postWithComments)
print("Post with id 123 deleted with success")
} catch let error as DataStoreError {
print("Failed with error \(error)")
} catch {
print("Unexpected error \(error)")
}
let sink = Amplify.Publisher.create {
guard let postWithComments = try await Amplify.DataStore.query(Post.self, byId: "123") else {
return
}
try await Amplify.DataStore.delete(postWithComments)
}.sink {
if case let .failure(error) = $0 {
print("Error deleting post and comments - \(error)")
}
} receiveValue: {
print("Post with id 123 deleted with success")
}

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.

enum PostStatus {
ACTIVE
INACTIVE
}
type Post @model {
id: ID!
title: String!
rating: Int
status: PostStatus
editors: [User] @manyToMany(relationName: "PostEditor")
}
type User @model {
id: ID!
username: String!
posts: [Post] @manyToMany(relationName: "PostEditor")
}
let post = Post(
title: "My first post",
status: .active
)
let user = User(
username: "Nadia"
)
let postEditor = PostEditor(
post: post,
user: user
)
do {
try await Amplify.DataStore.save(post)
try await Amplify.DataStore.save(user)
try await Amplify.DataStore.save(postEditor)
print("Saved post, user, and postEditor!")
} catch let error as DataStoreError {
print("Failed with error \(error)")
} catch {
print("Unexpected error \(error)")
}
let post = Post(
title: "My first post",
status: .active
)
let user = User(
username: "Nadia"
)
let postEditor = PostEditor(
post: post,
user: user
)
let sink = Amplify.Publisher.create{ try await Amplify.DataStore.save(post) }
.flatMap { _ in
Amplify.Publisher.create { try await Amplify.DataStore.save(user) }
}
.flatMap { _ in
Amplify.Publisher.create { try await Amplify.DataStore.save(postEditor) }
}
.sink {
if case let .failure(error) = $0 {
print("Error saving post, user and postEditor: \(error.localizedDescription)")
}
}
receiveValue: { _ in
print("Saved user, post and postEditor!")
}

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.