Page updated Jan 16, 2024

Customize primary keys

Customize primary keys

By default, DataStore models have an id field that is automatically populated on the client with a UUID v4, allowing DataStore to generate non-colliding globally unique identifiers in a scalable way. While UUIDs have desirable properties (they are large, non-sequential and opaque), there are times when a custom primary key, also known as custom identifier, is needed. For instance, to:

  • Have friendly/readable identifiers (surrogate/opaque vs. natural keys)
  • Define composite primary keys
  • Customize data partitioning to optimize for scale (especially important when planning to handle large amounts of data in short periods of time)
  • Selectively synchronize data to clients (e.g. by fields like deviceId, userId or similar)
  • Prioritize the sort order in which objects are returned by the sync queries
  • Make existing data consumable and syncable by DataStore

A schema with the typical id field looks like this:

1type Book @model {
2 id: ID!
3 title: String!
4 description: String
5}

You can customize the primary key by adding the @primaryKey directive to a field:

1type Book @model {
2 isbn: ID! @primaryKey
3 title: String!
4 description: String
5}

You can also require multiple fields to define your primary key. When your primary key references multiple fields, it's called a composite key. In the example below, the primary key is defined by the isbn and title fields:

1type Book @model {
2 isbn: ID! @primaryKey(sortKeyFields: ["title"])
3 title: String!
4 description: String
5}

Determine when the primary key field is auto-populated upon record creation

When you create a record with DataStore, a UUID is automatically populated for the default id: ID! primary key field. When working with custom primary keys, DataStore will automatically populate the key fields in the following conditions:

DescriptionTypeAutopopulated with UUID

Without @primaryKey

1type Customer @model {
2 firstName: String
3 lastName: String
4}
✅ Yes

Without @primaryKey, explicit id field

1type Customer @model {
2 id: ID!
3 firstName: String
4 lastName: String
5}
✅ Yes

@primaryKey on a custom field

1type Customer @model {
2 customerId: ID! @primaryKey
3 firstName: String
4 lastName: String
5}
❌ No

Explicit @primaryKey on id field

1type Customer @model {
2 id: ID! @primaryKey
3 dob: AWSDateTime!
4 firstName: String
5 lastName: String
6}
✅ Yes

Explicit @primaryKey on id field with sort key

1type Customer @model {
2 id: ID! @primaryKey(sortKeyFields: ["dob"])
3 dob: AWSDateTime!
4 firstName: String
5 lastName: String
6}
✅ Yes

Explicit id field in sort key

1type Customer @model {
2 country: String! @primaryKey(sortKeyFields: ["id"])
3 id: ID!
4 firstName: String
5 lastName: String
6}
✅ Yes

@primaryKey with no id field

1type Customer @model {
2 zip: String! @primaryKey(sortKeyFields: ["username"])
3 username: String!
4 firstName: String
5 lastName: String
6}
❌ No

Querying records with custom primary keys

A record with a custom primary key can be queried for in the following ways:

With the value of the primary key:

1Amplify.DataStore.query(Book.self, byIdentifier: .identifier(isbn: "12345")) { result in
2 switch result {
3 case .success(let book):
4 guard let book = book else {
5 print("Query was successful with empty result")
6 return
7 }
8 print("Query was successful, retrieved book: \(book)")
9 case .failure(let error):
10 print("Error on query() for type Book with error: \(error)")
11 }
12}
1let sink = Amplify.DataStore.query(Book.self, byIdentifier: .identifier(isbn: "12345")).sink {
2 if case let .failure(error) = $0 {
3 print("Error on query() for type Book with error: \(error)")
4 }
5} receiveValue:{ book in
6 guard let book = book else {
7 print("Query was successful with empty result")
8 return
9 }
10 print("Query was successful, retrieved book: \(book)")
11}

With the value of QueryPredicate:

1Amplify.DataStore.query(Book.self, where: Book.keys.isbn == "12345") { result in
2 switch result {
3 case .success(let books):
4 print("Query was successful, retrieved books: \(books)")
5 case .failure(let error):
6 print("Error on query() for type Book with error: \(error)")
7 }
8}
1let sink = Amplify.DataStore.query(Book.self, where: Book.keys.isbn == "12345").sink {
2 if case let .failure(error) = $0 {
3 print("Error on query() for type Book with error \(error)")
4 }
5} receiveValue: { books in
6 print("Query was successful, retrieved books: \(books)")
7}

Deleting records with custom primary keys

A record with a custom primary key can be deleted in the following ways:

With the value of the primary key:

1Amplify.DataStore.delete(Book.self, withIdentifier: .identifier(isbn: "12345")) { result in
2 switch result{
3 case .success:
4 print("Book deleted!")
5 case .failure(let error):
6 print("Error deleting book: \(error)")
7 }
8}
1let sink = Amplify.DataStore.delete(Book.self, withIdentifier: .identifier(isbn: "12345")).sink {
2 if case let .failure(error) = $0 {
3 print("Error deleting book: \(error)")
4 }
5} receiveValue: {
6 print("Book deleted!")
7}

With the value of QueryPredicate:

1Amplify.DataStore.delete(Book.self, where: Book.keys.isbn == "12345") { result in
2 switch result {
3 case .success:
4 print("Book deleted!")
5 case .failure(let error):
6 print("Error deleting book: \(error)")
7 }
8}
1let sink = Amplify.DataStore.delete(Book.self, where: Book.keys.isbn == "12345").sink {
2 if case let .failure(error) = $0 {
3 print("Error deleting book: \(error)")
4 }
5} receiveValue: {
6 print("Book deleted!")
7}