Page updated Jan 16, 2024

Manipulating data

Create and update

To write data to the DataStore, pass an instance of a model to Amplify.DataStore.save():

1do {
2 try await Amplify.DataStore.save(
3 Post(title: "My first post",
4 status: .active,
5 content: "Amplify.DataStore is awesome!")
6 )
7 print("Created a new post successfully")
8} catch let error as DataStoreError {
9 print("Error creating post - \(error)")
10} catch {
11 print("Unexpected error \(error)")
12}
1let saveSink = Amplify.Publisher.create {
2 try await Amplify.DataStore.save(
3 Post(title: "My first post",
4 status: .active,
5 content: "Amplify.DataStore is awesome!")
6 )}.sink {
7 if case let .failure(error) = $0 {
8 print("Error updating post - \(error.localizedDescription)")
9 }
10 } receiveValue: {
11 print("Updated the existing post: \($0)")
12 }

The save method creates a new record, or in the event that one already exists in the local store, it updates the record.

1var existingPost: Post = /* get an existing post */
2existingPost.title = "[updated] My first post"
3
4do {
5 try await Amplify.DataStore.save(existingPost)
6 print("Updated the existing post")
7} catch let error as DataStoreError {
8 print("Error updating post - \(error)")
9} catch {
10 print("Unexpected error \(error)")
11}
1var existingPost: Post = /* get an existing post */
2existingPost.title = "[updated] My first post"
3let postForUpdate = existingPost
4let saveSink = Amplify.Publisher.create {
5 try await Amplify.DataStore.save(existingPost)
6}.sink {
7 if case let .failure(error) = $0 {
8 print("Error updating post - \(error)")
9 }
10}
11receiveValue: {
12 print("Updated the existing post: \($0)")
13}

Avoid working with stale data!

Model instances which store values, such as those from the results of a DataStore.Query operation, can become stale and outdated when their properties are updated. This can be the result of a manual change or even a side effect of real time data being received by the application. In order to ensure you are performing mutations on the latest committed state to the system, either perform a query directly before the DataStore.save() operation or observe the model to keep the state updated at all times and perform mutations on that latest state referencing the model instance. The preceding example demonstrates one approach. The following example demonstrates the observeQuery approach.

1import SwiftUI
2import Amplify
3import Combine
4
5/*
6 Example showing how to observe the model and keep the state updated before performing a save. This uses the
7 `@Published` property for views to observe and re-render changes to the model.
8 */
9class PostViewModel: ObservableObject {
10 @Published var post: Post?
11 var subscription: AnyCancellable?
12
13 init() {
14 }
15
16 func observe(postId: String) {
17 self.subscription = Amplify.Publisher.create(
18 Amplify.DataStore.observeQuery(
19 for: Post.self,
20 where: Post.keys.id == postId
21 )
22 )
23 .sink { completion in
24 print("Completion event: \(completion)")
25 } receiveValue: { snapshot in
26 guard let post = snapshot.items.first else {
27 return
28 }
29 DispatchQueue.main.async {
30 self.post = post
31 }
32 }
33 }
34
35 func updateTitle(_ title: String) async {
36 guard var post = post else {
37 return
38 }
39 post.title = title
40 do {
41 let updatedPost = try await Amplify.DataStore.save(post)
42 print("Updated post successfully: \(updatedPost)")
43 } catch let error as DataStoreError {
44 print("Failed to update post: \(error)")
45 } catch {
46 print("Unexpected error \(error)")
47 }
48 }
49}
50struct PostView: View {
51 @StateObject var vm = PostViewModel()
52 @State private var title = ""
53 let postId: String
54
55 init(postId: String) {
56 self.postId = postId
57 }
58
59 var body: some View {
60 VStack {
61 Text("Post's current title: \(vm.post?.title ?? "")")
62 TextField("Enter new title", text: $title)
63 Button("Click to update the title to '\(title)'") {
64 Task { await vm.updateTitle(title) }
65 }
66 }.onAppear(perform: {
67 Task { await vm.observe(postId: postId) }
68 })
69 }
70}

Delete

To delete an item, simply pass in an instance.

1do {
2 try await Amplify.DataStore.delete(post)
3 print("Post deleted!")
4} catch let error as DataStoreError {
5 print("Error deleting post - \(error)")
6} catch {
7 print("Unexpected error \(error)")
8}
1let sink = Amplify.Publisher.create {
2 try await Amplify.DataStore.delete(post)
3}.sink {
4 if case let .failure(error) = $0 {
5 print("Fetch session failed with error \(error)")
6 }
7}
8receiveValue: {
9 print("Post deleted!")
10}

A delete can also be achieved by a type and its id.

1do {
2 try await Amplify.DataStore.delete(Post.self, withId: "123")
3 print("Post deleted!")
4} catch let error as DataStoreError {
5 print("Error deleting post - \(error)")
6} catch {
7 print("Unexpected error \(error)")
8}
1let sink = Amplify.Publisher.create {
2 try await Amplify.DataStore.delete(Post.self, withId: "123")
3}.sink {
4 if case let .failure(error) = $0 {
5 print("Error deleting post - \(error)")
6 }
7}
8receiveValue: {
9 print("Post deleted!")
10}

Query Data

Queries are performed against the local store. When cloud synchronization is enabled, the local store is updated in the background by the DataStore Sync Engine.

For more advanced filtering, such as matching arbitrary field values on an object, you can supply a query predicate.

1do {
2 let result = try await Amplify.DataStore.query(Post.self)
3 // result will be of type [Post]
4 print("Posts: \(result)")
5} catch let error as DataStoreError {
6 print("Error on query() for type Post - \(error)")
7} catch {
8 print("Unexpected error \(error)")
9}
1let sink = Amplify.Publisher.create {
2 try await Amplify.DataStore.query(Post.self)
3}.sink {
4 if case let .failure(error) = $0 {
5 print("Error on query() for type Post - \(error)")
6 }
7}
8receiveValue: { result in
9 print("Posts: \(result)")
10}

Query by identifier

Query has an alternative signature that allows to fetch a single item by its id:

1do {
2 let result = try await Amplify.DataStore.query(Post.self, byId: "123")
3 // result will be a single object of type Post?
4 print("Post: \(result)")
5} catch {
6 print("Error on query() for type Post - \(error)")
7}
1let sink = Amplify.Publisher.create {
2 try await Amplify.DataStore.query(Post.self, byId: "123")
3}.sink {
4 if case let .failure(error) = $0 {
5 print("Error on query() for type Post - \(error)")
6 }
7}
8receiveValue: { result in
9 print("Posts: \(result)")
10}

Predicates

Predicates are filters that can be used to match items in the DataStore. When applied to a query(), they constrain the returned results. When applied to a save(), they act as a pre-requisite for updating the data. You can match against fields in your schema by using the following predicates:

Strings: eq | ne | le | lt | ge | gt | contains | notContains | beginsWith | between

Numbers: eq | ne | le | lt | ge | gt | between

Lists: contains | notContains

For example if you wanted a list of all Post Models that have a rating greater than 4:

1let p = Post.keys
2do {
3 let result = try await Amplify.DataStore.query(Post.self, where: p.rating > 4)
4 print("Posts: \(result)")
5} catch let error as DataStoreError {
6 print("Error listing posts - \(error)")
7} catch {
8 print("Unexpected error \(error)")
9}
1let p = Post.keys
2let sink = Amplify.Publisher.create {
3 try await Amplify.DataStore.query(Post.self, where: p.rating > 4)
4}.sink {
5 if case let .failure(error) = $0 {
6 print("Error listing posts - \(error)")
7 }
8}
9receiveValue: { result in
10 print("Posts: \(result)")
11}

Multiple conditions can also be used, like the ones defined in GraphQL Transform condition statements. For example, fetch all posts that have a rating greater than 4 and are ACTIVE:

1let p = Post.keys
2do {
3 let result = try await Amplify.DataStore.query(
4 Post.self,
5 where: p.rating > 4 && p.status == PostStatus.active
6 )
7 // result of type [Post]
8 print("Published posts with rating greater than 4: \(result)")
9} catch let error as DataStoreError {
10 print("Error listing posts - \(error)")
11} catch {
12 print("Unexpected error \(error)")
13}
1let p = Post.keys
2let sink = Amplify.Publisher.create {
3 try await Amplify.DataStore.query(
4 Post.self,
5 where: p.rating > 4 && p.status == PostStatus.active
6 )
7}.sink {
8 if case let .failure(error) = $0 {
9 print("Error listing posts - \(error)")
10 }
11}
12receiveValue: { result in
13 print("Published posts with rating greater than 4: \(result)")
14}

You can also write this in a compositional function manner by replacing the operators with their equivalent predicate statements such as .gt, .and, etc:

1let p = Post.keys
2try await Amplify.DataStore.query(
3 Post.self,
4 where: p.rating.gt(4).and(p.status.eq(PostStatus.active))
5)
1let p = Post.keys
2let sink = Amplify.Publisher.create {
3 try await Amplify.DataStore.query(
4 Post.self,
5 where: p.rating > 4 && p.status == PostStatus.active
6 )
7}.sink {
8 // handle result
9}

Alternatively, the or logical operator can also be used:

1let p = Post.keys
2do {
3 let result = try await Amplify.DataStore.query(
4 Post.self,
5 where: p.rating == nil || p.status == PostStatus.active
6 )
7 // result of type [Post]
8 print("Posts in draft or without rating: \(result)")
9} catch let error as DataStoreError {
10 print("Error listing posts - \(error)")
11} catch {
12 print("Unexpected error \(error)")
13}
1let p = Post.keys
2let sink = Amplify.Publisher.create {
3 try await Amplify.DataStore.query(
4 Post.self,
5 where: p.rating == nil || p.status == PostStatus.active
6 )
7}.sink {
8 if case let .failure(error) = $0 {
9 print("Error listing posts - \(error)")
10 }
11}
12receiveValue: { result in
13 // result of type [Post]
14 print("Posts in draft or without rating: \(result)")
15}

Sort

Query results can also be sorted by one or more fields.

For example, to sort all Post objects by rating in ascending order:

1do {
2 let result = try await Amplify.DataStore.query(
3 Post.self,
4 sort: .ascending(Post.keys.rating))
5 print("Posts: \(result)")
6} catch let error as DataStoreError {
7 print("Error listing posts - \(error)")
8} catch {
9 print("Unexpected error \(error)")
10}
1let sink = Amplify.Publisher.create {
2 try await Amplify.DataStore.query(
3 Post.self,
4 sort: .ascending(Post.keys.rating))
5}.sink {
6 if case let .failure(error) = $0 {
7 print("Error listing posts - \(error)")
8 }
9}
10receiveValue: { result in
11 print("Posts: \(result)")
12}

To get all Post objects sorted first by rating in ascending order, and then by title in descending order:

1do {
2 let result = try await Amplify.DataStore.query(
3 Post.self,
4 sort: .by(
5 .ascending(Post.keys.rating),
6 .descending(Post.keys.title)
7 )
8 )
9 print("Posts: \(result)")
10} catch let error as DataStoreError {
11 print("Failed with error \(error)")
12} catch {
13 print("Error listing posts - \(error)")
14}
1let sink = Amplify.Publisher.create {
2 try await Amplify.DataStore.query(
3 Post.self,
4 sort: .by(
5 .ascending(Post.keys.rating),
6 .descending(Post.keys.title)
7 )
8 )
9}.sink {
10 if case let .failure(error) = $0 {
11 print("Error listing posts - \(error)")
12 }
13}
14receiveValue: { result in
15 print("Posts: \(result)")
16}

Pagination

Query results can also be paginated by passing in a page number (starting at 0) and an optional limit (defaults to 100). This will return a list of the first 100 items:

1let posts = try await Amplify.DataStore.query(
2 Post.self,
3 paginate: .page(0, limit: 100))
1let sink = Amplify.Publisher.create {
2 try await Amplify.DataStore.query(
3 Post.self,
4 paginate: .page(0, limit: 100))
5}.sink {
6 // handle result
7}

The paginate arguments takes an object of type QueryPaginationInput. That object can be created with the following factory functions:

Query, then observe changes

To both query and observe subsequent changes to a Model, consider using observeQuery.