Introduction
One of the common tasks when working with databases is syncing related data. For example, if you have a user model with many posts, and you want to update the user's posts based on new data, you need to sync the new data with the existing posts. AdonisJs provides several ways to work with related data, including eager loading, lazy loading, and preloading. However, syncing related data can still be a challenge, especially when dealing with nested relations.
In this blog post, we'll look at how to create a syncRelation function for AdonisJs 5 that makes it easy to sync related data, even with nested relations.
The syncRelation function
The syncRelation function is a utility function that allows you to sync related data with a related model's data. It takes four parameters:
relation: The relation to sync. This should be a QueryBuilder instance that represents the relation between two models.
data: The data to sync with the relation. This should be an array of ModelObject instances that represent the data to sync.
cb: The callback function to use to recursively sync related data. This function takes two parameters: the data to sync and the related model to sync it with. The callback function should return a Promise that resolves when the related data has been synced.
allowDelete: Whether or not to allow deleting related data that is not in the given data. This parameter is optional and defaults to false.
async function syncRelation<T extends LucidModel, U extends LucidModel>(<br />
relation: QueryBuilder<T, U>,<br />
data: ModelObject<U>[],<br />
cb: (data: ModelObject<U>, model: U) => Promise<void>,<br />
allowDelete = false<br />
): Promise<void>
Let's look at each parameter in more detail.
The relation parameterThe relation parameter is a QueryBuilder instance that represents the relation between two models. This can be any type of relation, including hasOne, hasMany, belongsTo, and belongsToMany. The QueryBuilder instance allows you to fetch, create, update, and delete related data.
The data parameterThe data parameter is an array of ModelObject instances that represent the data to sync with the relation. Each ModelObject instance should have a property that corresponds to the primary key of the related model. For example, if you're syncing posts with a user, each post ModelObject should have an id property that corresponds to the id of the related user.
The cb parameterThe cb parameter is a callback function that is used to recursively sync related data. This function takes two parameters: the data to sync and the related model to sync it with. The data parameter is a ModelObject instance that represents the data to sync, and the model parameter is a related model instance that represents the data in the database. The cb function should modify the model instance to reflect the data in the data instance. It can also call the syncRelation function again to sync any nested relations. The cb function should return a Promise that resolves when the related data has been synced.
The allowDelete parameterThe allowDelete parameter is an optional boolean parameter that specifies whether or not to allow deleting related data that is not in the given data. If set to true, the syncRelation function will delete any related models that are not in the data parameter. This can be useful when you want to delete any related data that has been removed from the source data.
Example usage
import Database from '@ioc:Adonis/Lucid/Database'
import { LucidModel, ModelObject, QueryBuilder } from '@ioc:Adonis/Lucid/Orm'
async function syncRelation<T extends LucidModel, U extends LucidModel>(
relation: QueryBuilder<T, U>,
data: ModelObject<U>[],
cb: (data: ModelObject<U>, model: U) => Promise<void>,
allowDelete = false
): Promise<void> {
await Database.transaction(async (trx) => {
const existingData = await relation.transacting(trx).fetch()
for (const existingModel of existingData.rows) {
const newData = data.find((d) => d.id === existingModel.id)
if (!newData && allowDelete) {
await existingModel.delete()
} else if (newData) {
await cb(newData, existingModel)
}
}
for (const newData of data) {
const existingModel = existingData.rows.find((m) => m.id === newData.id)
if (!existingModel) {
await relation.transacting(trx).create(newData)
}
}
})
}
This implementation uses the Database.transaction method to ensure that all updates are done atomically. It fetches the existing data from the database, iterates over it, and updates or deletes any related models as necessary. It then iterates over the new data and creates any missing models.
Here's how we can use the syncRelation function to sync a user's posts and comments:
import User from 'App/Models/User'
const user = await User.find(1)
const data = {
posts: [
{
id: 1,
title: 'New title',
body: 'New body',
comments: [
{
id: 1,
body: 'New comment',
user: {
id: 1,
name: 'New name',
email: 'newemail@example.com',
},
},
],
},
],
}
await syncRelation(user.posts(), data.posts, async (postData, postModel) => {
postModel.title = postData.title
postModel.body = postData.body
await syncRelation(postModel.comments(), postData.comments, async (commentData, commentModel) => {
commentModel.body = commentData.body
await syncRelation(commentModel.user(), commentData.user, async (userData, userModel) => {
userModel.name = userData.name
userModel.email = userData.email
})
})
}, true)
In this example, we're syncing a user's posts and comments based on the data object we received. The data object has a posts array, each element of which has a comments array, each element of which has a user object. We use the syncRelation function to sync each level of the nested relations.
When we call syncRelation(user.posts(), data.posts, ...), the cb function is called for each post that already exists in the database. We update the post's title and body based on the data object. We then call syncRelation(postModel.comments(), postData.comments, ...) to sync the comments for the post.
The cb function for the comments updates the comment's body, and then calls syncRelation(commentModel.user(), commentData.user, ...) to sync the user for the comment. Finally, the cb function for the user updates the user's name and email.
If any related data has been removed from the data object (e.g. a comment has been deleted), the allowDelete parameter allows us to delete the related model from the database.
Conclusion
In this article, we've looked at how to implement a syncRelation function in AdonisJS 5 to sync related data based on a given data object. The syncRelation function can be used to update or create related models, and it can handle nested relations as well.
Using the syncRelation function can make it easier to keep related data in sync when updating models based on incoming data. By handling related data in a consistent and reliable way, we can avoid bugs and ensure that our application's data remains consistent.

