The schema part of the associated code
In the schema we care about two pieces of information: the type of the association id (type), and which collection the association is (ref). The association of mongoose can only store id, and cannot store other fields because it is not supported. If you want to store fields other than id, you need to write the corresponding logic to get the fields.
If it is to many, it is necessary to use
association attribute s: [{ }]
, if it is one to one, it is necessary to UseAssociative property: { }
.
Example for many-to-many bidirectional association:
1
2
3
4
5//schema 1
courses: [{
type: mongoose.Types.ObjectId, //If the id type is String, write String.
ref: 'Course' //The model name associated with the collection, note that it is capitalized.
}]1
2
3
4
5//schema 2
students: [{
type: mongoose.Types.ObjectId,
ref: 'Student'
}]
The controller part of the associated code
Divided into two parts: add association and delete association.
Add association
Note here that in a many-to-many relationship,
addCourseToStudent()
andaddStudentToCourse()
are the same thing. Here you need to get the id information of the two parties you want to associate, and this information is written on the url:/v1/students/:id/courses/:code
. If it is to add an association, the method is POST.What needs to be guaranteed is that the variable names (
:id
,:code
) on the url must be consistent with those inconst { } = req.params
.After getting the id, first verify whether the (two) ids exist. In addition, since two id objects need to be used to verify the existence of the function, it is necessary to import the schema of another id object, such as
const Course = require('../modules/course');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20async function addCourseToStudent(req, res){
const { id, code } = req.params; //Take two ids
const student = await Student.findById(id).exec(); //Verify that id1 exists
const course = await Course.findById(code).exec(); //Verify that id2 exists
if (!student || !course) { //Process when there is no id
return res.status(404).json({
error: 'Category or Subcategory not found',
});
}
student.courses.addToSet(course._id); //It is recommended to use addToSet instead of push to avoid repetition
course.students.addToSet(student._id); //Two-way binding
//If it is a one-to-one relationship, that is, the associated id is not an array, it only needs to be written as student.course = course._id
await student.save(); //save
await course.save(); //save
return res.json(student);
}
After writing, export this function and register the route in the routing file.
1
studentRouter.post('/:id/courses/:code', addCourseToStudent)
Remove association
is very similar to adding. Just change the function name in the add code to
removeCourseToStudent
and thenaddToSet()
topull()
. Also, the request method should be changed from POST to DELETE.There are two logics for deletion, one is to delete whether there is a connection or not; the other is to detect whether there is a connection first, and then delete it. Usually don’t care, because the front end will know whether there is a relationship when fetching data, and the delete option will pop up when there is a relationship.
If it is a one-to-one relationship, simply reassign the resource of ref to undefined.
Schema and path issues for two-way binding
In two-way binding, the ref in the schema must be written by both parties. However, in the controller and router, only one of them needs to be coded, because when the unilateral controller is coded, the schema of both sides will be directly introduced, and _id will be added to both sides and saved on both sides.
Get the associated data after creating the association: populate()
In the
getXXXById()
function in the controller file, addpopulate('field')
after thefindById(id)
function.1
2
3
4
5
6async function getStudentById(req, res){
const {id} = req.params;
const student = await Student.findById(id).populate('course').exec();
if(!student){return ...};
res.json(student);
}If the document corresponding to the query id is not associated,
populate()
will not report an error, but only the associated item will not be displayed.The principle here is that the principle of populate is that mongoose sends two requests to the database and splices them together. This is determined by the database structure of mongoose. All data cannot be obtained in one request, so splicing exists on the server side. Instead, sql is stitched together in the database. But mongodb also supports database-side splicing processing, which uses aggregation. Later optimization may be used, and populate can be used in the initial stage.
populate()
also supports fetching only a subset of fields. Same as MongoDB section 2.3.5. (id will be retrieved by default)1
const student = await Student.findById(id).populate('course','name description') .exec();
populate()
can also fetch multiple nested data with two-paragraph chaining, but this is not recommended. However, this is the only way to fetch multiple associated resources. Because a singlepopulate()
can only be selected from the same resource, even with multiple attributes.
Scenarios for populate()
When fetching all the data, generally only metadata is needed, so there is no need to populate (resource waste). Generally,
populate()
is done when fetching a specific document.If two resources are associated and you want to fetch them at the same time, you can call
populate()
in a chain.
Decide which fields populate() selects in the front end
Sometimes there are too many fields associated with the resource, and the required fields must be selected from the front end (API control).
1
2
3
4
5
6
7async function getStudentById(req, res){
const { id } = req.params;
const { fields } = req. query;
const student = await Student.findById(id).select('').populate('course').exec(); //Get the student's
if(!student){return ...};
res.json(student);
}
Remove related associations when deleting resources
When mongoose finds that the associated resource does not exist, it will not report an error. But this results in a lot of invalid data being present.
1
2
3
4
5
6
7
8
9
10
11
12async function deleteStudentById(req, res){
const {id} = req.params;
const student = await Student.findByIdAndDelete(id).exec();
if(!student){return...};
await Course.updateMany({ students:student._id}, { //:?.?
$pull:{
students:student._id
}
}).exec();
res.sendStatus(204);
}Both sides of a bidirectional association delete the association. The code logic is the same.
In the one-to-one relationship, because it is not an array,
$pull
cannot be used, otherwise the error “MongoServerError: Cannot apply $pull to a non-array value” will appear. The$unset
operator should be used instead, with the same syntax.1
2
3
4
5
6
7
8
9
10
11
12async function deleteRootCategoryId(req, res) {
const { id } = req.params;
const rootCategory = await RootCategory.findByIdAndDelete(id).exec();
if (!rootCategory) {}
await SubCategory.updateMany({ parentCategory: rootCategory._id }, {
$unset: {
parentCategory: rootCategory._id
}
}).exec()
res.sendStatus(204);
}
Summary
The associated code is divided into three parts: schema (two-way and two-way), controller (two-way one-way/two-way), and router (two-way one-way).
In the controller, there are three parts to be added and rewritten:
Add an association function (two-way and one-way)
Delete the associated function (two-way and one-way)
The function to find a single resource plus
populate()
(both sides)Delete the function of a single resource plus delete the associated code (both sides)