Duplicate Fields
"Duplicate fields" is a rest-hapi feature that allows fields from an associated document to exist in the parent document while maintaining the original field value. This can be accomplished by setting config.enableDuplicateFields
to true
and adding the duplicate
property to an association definition.
Basic example
In the code below, the name
field of the role model will be duplicated in the user model:
// models/role.model.js
module.exports = function (mongoose) {
let modelName = "role";
let Types = mongoose.Schema.Types;
let Schema = new mongoose.Schema({
name: {
type: Types.String,
required: true
},
description: {
type: Types.String
}
}, { collection: modelName });
Schema.statics = {
collectionName:modelName,
routeOptions: {
associations: {
users: {
type: "ONE_MANY",
alias: "user",
foreignField: "role",
model: "user"
}
}
}
};
return Schema;
};
// models/user.model.js
module.exports = function (mongoose) {
let modelName = "user";
let Types = mongoose.Schema.Types;
let Schema = new mongoose.Schema({
email: {
type: Types.String,
unique: true
},
role: {
type: Types.ObjectId,
ref: "role"
}
}, { collection: modelName });
Schema.statics = {
collectionName:modelName,
routeOptions: {
associations: {
role: {
type: "MANY_ONE",
model: "role",
duplicate: ['name'] <--- list duplicate fields
}
}
}
};
return Schema;
};
NOTE: Only associations of type
MANY_ONE
andONE_ONE
can have theduplicate
property.
Given these model definitions, lets say we have the following role documents:
{
"_id": "59efe15e20905150d340b56a",
"name": "User",
"description": "A standard user account."
},
{
"_id": "59efe15e20905150d340b56b",
"name": "Admin",
"description": "A user with advanced permissions."
},
Now lets create a user document and assign it to the User
role with the following payload:
{
"email": "test@user.com",
"role": "59efe15e20905150d340b56a"
}
Finally, when we retrieve the user document with a GET, we should recieve an object similar to the one below:
{
"_id": "59efe15e20905150d340b56c"
"email": "test@user.com",
"role": "59efe15e20905150d340b56a",
"roleName": "User"
}
Note that we did not have to manually embed the roleName
property. It was automatically populated when the user was created due to the duplicate
property in the user-role association definition.
Lets say we decide to promote the user to the Admin
role by updating the user document with the following payload:
{
"role": "59efe15e20905150d340b56b"
}
Now when we retrieve the user document, we will see:
{
"_id": "59efe15e20905150d340b56c"
"email": "test@user.com",
"role": "59efe15e20905150d340b56b",
"roleName": "Admin"
}
The roleName
duplicate field was automatically updated to reflect the association change!
Tracking duplicated fields
In the above example, we showed how the roleName
duplicate field could automatically update when the user's role
property changed. However what if the associated role document's name
property was updated? By default, the user's roleName
property will remain the same even if the original field value changes. However, by setting config.trackDuplicatedFields
to true
, rest-hapi will track changes from the original field and update ALL associated duplicate fields. For example, if we have the following user documents:
{
"_id": "59efe15e20905150d340b56d",
"email": "test@admin1.com",
"role": "59efe15e20905150d340b56b",
"roleName": "Admin"
},
{
"_id": "59efe15e20905150d340b56e",
"email": "test@admin2.com",
"role": "59efe15e20905150d340b56b",
"roleName": "Admin"
}
and we update the associated role document to be:
{
"_id": "59efe15e20905150d340b56b",
"name": "SuperUser",
"description": "A user with advanced permissions."
}
if config.trackDuplicatedFields
is set to true
, then the user documents will now look like:
{
"_id": "59efe15e20905150d340b56d",
"email": "test@admin1.com",
"role": "59efe15e20905150d340b56b",
"roleName": "SuperUser"
},
{
"_id": "59efe15e20905150d340b56e",
"email": "test@admin2.com",
"role": "59efe15e20905150d340b56b",
"roleName": "SuperUser"
}
This of course can be very useful, as all duplicated fields will stay up-to-date regardless of which end is updated. However this can also be resource intensive if not planned carefully. For instance, if 1 million user docs are associated with the Admin
role, then 1 million extra documents will be updated whenever the name
field of the role document is updated.
Custom field name
As shown in the example above, duplicate field names have a default form of [association name] + [original field name] (Ex: roleName
). If we want to customize the duplicate field name, we can assign an array of objects to the duplicate
property rather than an array of strings. For example, given the user model's association definition below:
associations: {
role: {
type: "MANY_ONE",
model: "role",
duplicate: [{
field: 'name',
as: 'title'
}]
}
}
the user object from the previous example would have the form:
{
"email": "test@user.com",
"role": "59efe15e20905150d340b56e",
"title": "User"
}
Nested duplicate fields
One interesting property of duplicate fields is that they themselves can be duplicating a duplicate field. For an example, consider the user
, role
, and business
models below:
// models/business.model.js
module.exports = function (mongoose) {
let modelName = "business";
let Types = mongoose.Schema.Types;
let Schema = new mongoose.Schema({
name: {
type: Types.String,
required: true
},
description: {
type: Types.String
}
}, { collection: modelName });
Schema.statics = {
collectionName:modelName,
routeOptions: {
associations: {
roles: {
type: "ONE_MANY",
alias: "role",
foreignField: "business",
model: "role"
}
}
}
};
return Schema;
};
// models/role.model.js
module.exports = function (mongoose) {
let modelName = "role";
let Types = mongoose.Schema.Types;
let Schema = new mongoose.Schema({
name: {
type: Types.String,
required: true
},
description: {
type: Types.String
},
business: {
type: Types.ObjectId,
ref: "business"
},
businessName: {
type: Types.String
}
}, { collection: modelName });
Schema.statics = {
collectionName:modelName,
routeOptions: {
associations: {
business: {
type: "MANY_ONE",
model: "business",
duplicate: 'name'
},
users: {
type: "ONE_MANY",
alias: "user",
foreignField: "role",
model: "user"
}
}
}
};
return Schema;
};
// models/user.model.js
module.exports = function (mongoose) {
let modelName = "user";
let Types = mongoose.Schema.Types;
let Schema = new mongoose.Schema({
email: {
type: Types.String,
unique: true
},
role: {
type: Types.ObjectId,
ref: "role"
}
}, { collection: modelName });
Schema.statics = {
collectionName:modelName,
routeOptions: {
associations: {
role: {
type: "MANY_ONE",
model: "role",
duplicate: [{
field: 'name'
},{
field: 'businessName',
as: 'company'
}]
}
}
}
};
return Schema;
};
Given the relationships between these models, a set of associated documents might look like this:
business document
:
{
"_id": "59efe15e20905150d340b57a",
"name": "Test Business",
"about": "A business for testing."
}
role document
:
{
"_id": "59efe15e20905150d340b57b"
"name": "User",
"business": "59efe15e20905150d340b57a",
"businessName": "Test Business",
"description": "A standard user account.",
}
user document
:
{
"_id": "59efe15e20905150d340b56c"
"email": "test@user.com",
"role": "59efe15e20905150d340b57b",
"roleName": "User",
"company": "Test Business"
}
As you can see, the value for the user document's duplicate field company
can be traced back to the name
field for the business document. If config.trackDuplicatedFields
is set to true
, then updating the original name
field will cause both the role's businessName
field and the user's company
fields to update as well.
NOTE: If a duplicate field references another duplicate field, then the referenced field must exist in the model schema. See the
businessName
field of therole
model above.
Advantages
The duplicate fields feature may seem trivial or redundant considering the same information can be included in a GET request using the $embed query parameter, however duplicate fields come with some powerful advantages. Probably the most clear advantage is the potential for improving the readability of a document. In situations where querying for the association is not ideal or possible (Ex: observing the document within MongoDB), it is much easier to discern information about the document. For example:
doc1
:
{
"_id": "59efe15e20905150d340b56c"
"email": "test@user.com",
"role": "59efe15e20905150d340b56b"
}
vs
doc2
:
{
"_id": "59efe15e20905150d340b56c"
"email": "test@user.com",
"role": "59efe15e20905150d340b56b",
"roleName": "Admin"
}
In the second object it is immediately obvious which role the user is associated with.
While this is useful, arguably the biggest advantage duplicate fields provide is the improved querying. For example, in doc1
above, the document can be filtered by the role _id (Ex: GET /user?role=59efe15e20905150d340b56b ), but thats as far as it goes when it comes to querying users based on their role information. However with doc2
, the user can be filtered by its role name (Ex: GET /user?roleName=Admin ) sorted by its role name (Ex: GET /user?$sort=roleName ), or even be text searchable by its role name (Ex: GET /user?$term=Admin ).