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

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

type Book @model {
id: ID!
title: String!
description: String
}

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

type Book @model {
isbn: ID! @primaryKey
title: String!
description: String
}

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:

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

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

type Customer @model {
firstName: String
lastName: String
}
✅ Yes

Without @primaryKey, explicit id field

type Customer @model {
id: ID!
firstName: String
lastName: String
}
✅ Yes

@primaryKey on a custom field

type Customer @model {
customerId: ID! @primaryKey
firstName: String
lastName: String
}
❌ No

Explicit @primaryKey on id field

type Customer @model {
id: ID! @primaryKey
dob: AWSDateTime!
firstName: String
lastName: String
}
✅ Yes

Explicit @primaryKey on id field with sort key

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

Explicit id field in sort key

type Customer @model {
country: String! @primaryKey(sortKeyFields: ["id"])
id: ID!
firstName: String
lastName: String
}
✅ Yes

@primaryKey with no id field

type Customer @model {
zip: String! @primaryKey(sortKeyFields: ["username"])
username: String!
firstName: String
lastName: String
}
❌ 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:

Amplify.DataStore.query(Book.self, byIdentifier: .identifier(isbn: "12345")) { result in
switch result {
case .success(let book):
guard let book = book else {
print("Query was successful with empty result")
return
}
print("Query was successful, retrieved book: \(book)")
case .failure(let error):
print("Error on query() for type Book with error: \(error)")
}
}
let sink = Amplify.DataStore.query(Book.self, byIdentifier: .identifier(isbn: "12345")).sink {
if case let .failure(error) = $0 {
print("Error on query() for type Book with error: \(error)")
}
} receiveValue:{ book in
guard let book = book else {
print("Query was successful with empty result")
return
}
print("Query was successful, retrieved book: \(book)")
}

With the value of QueryPredicate:

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

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:

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

With the value of QueryPredicate:

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