Modeling relationships
When modeling application data, you often need to establish relationships between different data models. In Amplify Data, you can create one-to-many, one-to-one, and many-to-many relationships in your Data schema. On the client-side, Amplify Data allows you to lazy or eager load of related data.
Types of relationships
Relationship | Code | Description | Example |
---|---|---|---|
one to many | a.hasMany(...) & a.belongsTo(...) | Creates a one-to-many relationship between two models. | A Team has many Members. A Member belongs to a Team. |
one to one | a.hasOne(...) & a.belongsTo(...) | Creates a one-to-one relationship between two models. | A Customer has one Cart. A Cart belongs to one Customer. |
many to many | Two a.hasMany(...) & a.belongsTo(...) on join tables | Create two one-to-many relationships between the related models in a join table. | A Post has many Tags. A Tag has many Posts. |
Model one-to-many relationships
Create a one-to-many relationship between two models using the hasMany()
and belongsTo()
method. In the example below, a Team has many Members and a Member belongs to exactly one Team.
- Create a reference field called
teamId
on the Member model. This reference field's type MUST match the type of Team's identifier. In this case, it's an auto-generatedid: a.id().required()
field. - Add a relationship field called
team
that references theteamId
field. This allows you to query for the team information from the Member model. - Add a relationship field called
members
that references theteamId
field on the Member model.
const schema = a.schema({ Member: a.model({ name: a.string().required(), // 1. Create a reference field teamId: a.id(), // 2. Create a belongsTo relationship with the reference field team: a.belongsTo('Team', 'teamId'), }),
Team: a.model({ mantra: a.string().required(), // 3. Create a hasMany relationship with the reference field // from the `Member`s model. members: a.hasMany('Member', 'teamId'), }),}).authorization((allow) => allow.publicApiKey());
Create a "Has Many" relationship between records
val team = Team.builder() .mantra("Go Frontend!") .build()
Amplify.API.mutate(ModelMutation.create(team), { Log.i("MyAmplifyApp", "Added team with id: ${it.data.id}") val member = Member.builder() .name("Tim") .team(it.data) .build()
Amplify.API.mutate(ModelMutation.create(member), { Log.i("MyAmplifyApp", "Added Member with id: ${it.data.id}")}, { Log.e("MyAmplifyApp", "Create failed", it)}, ) }, { Log.e("MyAmplifyApp", "Create failed", it) })
Update a "Has Many" relationship between records
val newTeam = Team.builder() .mantra("Go Fullstack!") .build()
Amplify.API.mutate(ModelMutation.create(newTeam), { Log.i("MyAmplifyApp", "Added team with id: ${it.data.id}")
val updatingMember = existingMember.copyOfBuilder().team(it.data).build()
Amplify.API.mutate(ModelMutation.update(updatingMember), { Log.i("MyAmplifyApp", "Updated Member with id: ${it.data.id}")}, { Log.e("MyAmplifyApp", "Create failed", it)}, ) }, { Log.e("MyAmplifyApp", "Create failed", it) })
Delete a "Has Many" relationship between records
If your reference field is not required, then you can "delete" a one-to-many relationship by setting the relationship value to null
.
val memberWithRemovedTeam = existingMember.copyOfBuilder().team(null).build()
Amplify.API.mutate(ModelMutation.update(memberWithRemovedTeam), { Log.i("MyAmplifyApp", "Updated Member with id: ${it.data.id}")}, { Log.e("MyAmplifyApp", "Create failed", it)},)
Lazy load a "Has Many" relationship
Amplify.API.query( ModelQuery.get(Team::class.java, Team.TeamIdentifier("YOUR_TEAM_ID")), { suspend { try { val members = when (val membersModelList = it.data.members) { is LoadedModelList -> { // Eager loading loads the 1st page only. membersModelList.items }
is LazyModelList -> { var page = membersModelList.fetchPage() var loadedMembers = mutableListOf(page.items) // initial page of members // loop through all pages to fetch the full list of members while (page.hasNextPage) { val nextToken = page.nextToken page = membersModelList.fetchPage(nextToken) // add the page of members to the members variable loadedMembers += page.items } loadedMembers } } Log.i("MyAmplifyApp", "members: $members") } catch (error: ApiException) { Log.e("MyAmplifyApp", "Failed to fetch members", error) } } }, { Log.e("MyAmplifyApp", "Failed to fetch team")})
Eagerly load a "Has Many" relationship
Amplify.API.query( ModelQuery.get<Team, TeamPath>( Team::class.java, Team.TeamIdentifier("YOUR_TEAM_ID") ) { teamPath -> includes(teamPath.members) }, { val members = (it.data.members as? LoadedModelList<Member>)?.items }, { Log.e("MyAmplifyApp", "Failed to fetch team")})
Model a "one-to-one" relationship
Create a one-to-one relationship between two models using the hasOne()
and belongsTo()
methods. In the example below, a Customer has a Cart and a Cart belongs to a Customer.
- Create a reference field called
customerId
on the Cart model. This reference field's type MUST match the type of Customer's identifier. In this case, it's an auto-generatedid: a.id().required()
field. - Add a relationship field called
customer
that references thecustomerId
field. This allows you to query for the customer information from the Cart model. - Add a relationship field called
activeCart
that references thecustomerId
field on the Cart model.
const schema = a.schema({ Cart: a.model({ items: a.string().required().array(), // 1. Create reference field customerId: a.id(), // 2. Create relationship field with the reference field customer: a.belongsTo('Customer', 'customerId'), }), Customer: a.model({ name: a.string(), // 3. Create relationship field with the reference field // from the Cart model activeCart: a.hasOne('Cart', 'customerId') }),}).authorization((allow) => allow.publicApiKey());
Create a "Has One" relationship between records
To create a "has one" relationship between records, first create the parent item and then create the child item and assign the parent.
val customer = Customer.builder() .name("Rene") .build()
Amplify.API.mutate(ModelMutation.create(customer), { Log.i("MyAmplifyApp", "Added customer with id: ${it.data.id}") val cart = Cart.builder() .items(listOf("Tomato", "Ice", "Mint")) .customer(customer) .build()
Amplify.API.mutate(ModelMutation.create(cart), { Log.i("MyAmplifyApp", "Added Cart with id: ${it.data.id}")}, { Log.e("MyAmplifyApp", "Create failed", it)}, ) }, { Log.e("MyAmplifyApp", "Create failed", it) })
Update a "Has One" relationship between records
To update a "Has One" relationship between records, you first retrieve the child item and then update the reference to the parent to another parent. For example, to reassign a Cart to another Customer:
val newCustomer = Customer.builder() .mantra("Ian") .build()
Amplify.API.mutate(ModelMutation.create(newCustomer), { Log.i("MyAmplifyApp", "Added customer with id: ${it.data.id}")
val updatingCart = existingCart.copyOfBuilder().customer(it.data).build()
Amplify.API.mutate(ModelMutation.update(updatingCart), { Log.i("MyAmplifyApp", "Updated cart with id: ${it.data.id}")}, { Log.e("MyAmplifyApp", "Create failed", it)}, ) }, { Log.e("MyAmplifyApp", "Create failed", it) })
Delete a "Has One" relationship between records
You can set the relationship field to null
to delete a "Has One" relationship between records.
val cartWithRemovedCustomer = existingCart.copyOfBuilder().customer(null).build()
Amplify.API.mutate(ModelMutation.update(cartWithRemovedCustomer), { Log.i("MyAmplifyApp", "Updated cart with id: ${it.data.id}")}, { Log.e("MyAmplifyApp", "Create failed", it)},)
Lazy load a "Has One" relationship
Amplify.API.query( ModelQuery.get(Team::class.java, Team.TeamIdentifier("YOUR_TEAM_ID")), { suspend { try { val customer = when (val customerReference = cart.customer) { is LoadedModelReference -> { customerReference.value }
is LazyModelReference -> { customerReference.fetchModel() } } Log.i("MyAmplifyApp", "customer: $customer") } catch (error: ApiException) { Log.e("MyAmplifyApp", "Failed to fetch customer", error) } } }, { Log.e("MyAmplifyApp", "Failed to get team")})
Eagerly load a "Has One" relationship
val cart = Amplify.API.query( ModelQuery.get<Cart, CartPath>( Cart::class.java, Cart.CartIdentifier("YOUR_CART_ID") ) { cartPath -> includes(cartPath.customer) },{ val customer = (cart.customer as? LoadedModelReference)?.value },{ Log.e("MyAmplifyApp", "Failed to fetch cart", it) })
Model a "many-to-many" relationship
In order to create a many-to-many relationship between two models, you have to create a model that serves as a "join table". This "join table" should contain two one-to-many relationships between the two related entities. For example, to model a Post that has many Tags and a Tag has many Posts, you'll need to create a new PostTag model that represents the relationship between these two entities.
const schema = a.schema({ PostTag: a.model({ // 1. Create reference fields to both ends of // the many-to-many relationship postId: a.id().required(), tagId: a.id().required(), // 2. Create relationship fields to both ends of // the many-to-many relationship using their // respective reference fields post: a.belongsTo('Post', 'postId'), tag: a.belongsTo('Tag', 'tagId'), }), Post: a.model({ title: a.string(), content: a.string(), // 3. Add relationship field to the join model // with the reference of `postId` tags: a.hasMany('PostTag', 'postId'), }), Tag: a.model({ name: a.string(), // 4. Add relationship field to the join model // with the reference of `tagId` posts: a.hasMany('PostTag', 'tagId'), }),}).authorization((allow) => allow.publicApiKey());
Model multiple relationships between two models
Relationships are defined uniquely by their reference fields. For example, a Post can have separate relationships with a Person model for author
and editor
.
const schema = a.schema({ Post: a.model({ title: a.string().required(), content: a.string().required(), authorId: a.id(), author: a.belongsTo('Person', 'authorId'), editorId: a.id(), editor: a.belongsTo('Person', 'editorId'), }), Person: a.model({ name: a.string(), editedPosts: a.hasMany('Post', 'editorId'), authoredPosts: a.hasMany('Post', 'authorId'), }),}).authorization((allow) => allow.publicApiKey());
On the client-side, you can fetch the related data with the following code:
const client = generateClient<Schema>();
const { data: post } = await client.models.Post.get({ id: "SOME_POST_ID" });
const { data: author } = await post?.author();const { data: editor } = await post?.editor();
Model relationships for models with sort keys in their identifier
In cases where your data model uses sort keys in the identifier, you need to also add reference fields and store the sort key fields in the related data model:
const schema = a.schema({ Post: a.model({ title: a.string().required(), content: a.string().required(), // Reference fields must correspond to identifier fields. authorName: a.string(), authorDoB: a.date(), // Must pass references in the same order as identifiers. author: a.belongsTo('Person', ['authorName', 'authorDoB']), }), Person: a.model({ name: a.string().required(), dateOfBirth: a.date().required(), // Must reference all reference fields corresponding to the // identifier of this model. authoredPosts: a.hasMany('Post', ['authorName', 'authorDoB']), }).identifier(['name', 'dateOfBirth']),}).authorization((allow) => allow.publicApiKey());
Make relationships required or optional
Amplify Data's relationships use reference fields to determine if a relationship is required or not. If you mark a reference field as required, then you can't "delete" a relationship between two models. You'd have to delete the related record as a whole.
const schema = a.schema({ Post: a.model({ title: a.string().required(), content: a.string().required(), // You must supply an author when creating the post // Author can't be set to `null`. authorId: a.id().required(), author: a.belongsTo('Person', 'authorId'), // You can optionally supply an editor when creating the post. // Editor can also be set to `null`. editorId: a.id(), editor: a.belongsTo('Person', 'editorId'), }), Person: a.model({ name: a.string(), editedPosts: a.hasMany('Post', 'editorId'), authoredPosts: a.hasMany('Post', 'authorId'), }),}).authorization((allow) => allow.publicApiKey());