Instructor: Remember how we have a field
for the average rating on each tour document?
Well up until this point that field doesn't really hold
any meaningful data, does it?
But so let's now actually change that
and calculate average ratings in this lecture.
So storing a summary of a related data set
on the main data set is actually a very popular technique
in data modeling that I hadn't actually mentioned yet.
And this technique can actually be really helpful
in order to prevent constant queries
of the related data set.
So in our application a great example of this technique
is to store the average rating and the number of ratings
on each tour, so that we don't have to query the reviews
and calculate that average each time that we query
for all the tours, okay.
For example, that could become very useful
for a tour overview page in our front-end
where we really do not want to display all the reviews,
but still want to show a summary of these reviews,
like for example, the number of ratings and the average.
And actually we already have the fields for that
in our tour schema.
So right here at the beginning, I think.
Yeah, so we have the ratings average
and ratings quantity right here,
but right now they're only just some numbers
and of course, they are not the actual average
and the number of ratings, because we never
really calculated that at any point in our application,
but that's exactly what we're gonna change in this video.
So right now we're gonna calculate the average rating
and also the number of ratings of a tour
each time that a new review is added to that tour
or also when a review is updated or deleted,
because that's exactly the situations
when the number or the average might change, right.
So how are we actually going to implement this?
Well back here in the review model
we're gonna create a new function
which will take in a tour ID
and calculate the average rating and the number of ratings
that exist in our collection for that exact tour.
Then in the end the function will even update
the corresponding tour document.
Then in order to use that function
we will use middleware to basically call this function
each time that there is a new review or one is updated
or deleted, okay.
So let's now start by writing that function
and for that we're actually gonna write a static method
on our schema, and that's a feature of Mongoose
that we hadn't used yet.
So we only used instance method,
which we can call on documents
and they are also very useful,
but this time we're really going to use static methods.
Okay.
So again, these can be called on the model
directly, for example, like this review.calcStats,
okay.
And the way this works is of course reviewSchema
and then .statics and then the name of the function.
So calcAverageRatings
is equal to a function, which remember takes in a tour ID,
and that ID is of course for the tour
to which the current review belongs to.
And I know that at this point it's probably all sounds
kind of confusing, but don't worry
once we have all of this implemented
and also when we're testing it
it will then make a lot of sense in practice, okay?
Anyway in order to now actually do the calculation
we will again use the aggregation pipeline, right.
So let's remember that in our tool controller,
so right here we use the aggregation pipeline
to also create some statistics like this, okay.
So we used the aggregate method, which we called directly
on the model, okay.
So now in our instance method we can actually do
the exact same thing.
So in a static method like this these keywords
actually points to the current model.
So that's why we can use this.aggregate,
because remember we need to call this.aggregate
on the model directly, and that's exactly
why we're using a static method here in the first place.
Because again this now points to the model
and we need to call aggregate always on the model, okay.
So into aggregate we need to pass in an array
of all the stages that we want in aggregate, okay.
So what do we want to do first?
Well the first step should be to select all the reviews
that actually belong to the current tour
that was passed in as the argument.
Okay.
So our first stage is a match stage,
remember that,
and so in here we passed our filter object
and we can say tour equal to tour,
but we don't really need that, as you already know,
and so let's just remove this part, or actually
just to make it a bit less confusing here with the names
let's call this one here tour ID, okay.
So tourId in here,
okay.
So like this we only select a tour
that we actually want to update, okay.
Now in the next stage let's actually calculate
the statistics themselves,
and for that we use a group stage.
And in the group phase remember
the first field that we need to specify is the ID, so _id
and then the common field
that all of the documents have in common
that we want to group by and so that's again
going to be the tour.
So just like in our previous statistic calculation example
here we grouped by the difficulty.
So like this we calculate the statistics for easy
and statistics for medium
and then statistics for difficult tours.
So in this example, we grouped all the tours together
by their difficulty,
okay.
But here, of course we're grouping all the tours together
by tour.
Great.
Now the number of ratings,
remember how we do that,
so all we do is to basically add one
for each tour that we have,
so each tour that was matched in the previous step, okay.
So if there are five review documents for the current tour
then for each of these documents one will get added.
So then in the end the number of ratings will be five,
and again, because of course we have five review documents.
So that makes sense, right?
Then also the average rating, which just like before
we use the average operator and this needs to be inside
an object
and in here the name of the field
which is the rating, right.
So remember how each review has a rating field.
So that's where we want to calculate the average from.
Okay, and that's actually it.
So number and average of ratings
is exactly what we wanted to calculate in this aggregation.
Now keep in mind that this actually returns a promise
and so we need to await that and then store it
into a variable called stats.
Okay.
Then mark this one here as async as well,
give it a save
and so now we actually need to use that stats variable.
For now all I really want to do just to test this
is to actually log the statistics to the console.
Console.log
stats.
Okay, then in the later step we will actually want to,
as I mentioned before then update the tour document
with these statistics,
but I'm gonna leave that for a bit later,
because for now we actually need to call
this method here somewhere, because otherwise
the statistics here will never get called.
And remember how we said at the beginning
that we will do this using middleware
each time that a new review is created.
So let's implement that using reviewSchema pre
save
and then just our normal regular middleware function here.
And remember that in this kind of middleware
that this keyword points to the document
that is currently being saved.
So this points to
current review, okay.
So we will want to call the calcAverageRating function
using this.tour, right.
Now how are we actually going to call this function?
Remember how I said that
this function is available on the model.
So basically like this put
review.calcAverageRatings
and then as I said we want to use this.tour,
okay.
And again, because this is the current review
and this is then the tour ID that we're gonna pass
inside of the calcAverageRatings.
Now the problem is that at this point here in the code
the review variable is not yet defined.
Now you might think that the simple solution would be to
simply move this code in here after this review declaration,
but unfortunately that's not going to work,
because just like in Express this code here basically runs
in the sequence it is declared.
So if we were to put this code here
after the review declaration then this reviewSchema here
would not contain this middleware,
because we would then only be declaring it
after the review model was already created, okay,
but there is fortunately still a way around this
and that is to use this.constructor.
So this here still points to the model.
You know, basically again this is the current document
and the constructor is basically the model
who created that document.
So this here stands for the tour
and so we can then simply do it like this.
Then call next as always and that's actually it.
So let's now go ahead and test this.
And for doing that I will create a new tour first,
because the other ones that we already have
they have the average and number of ratings
already calculated and so we really
want to start from scratch here when testing this out.
So to create a new tour we need to be logged in
as administrators, and I think we currently are,
but just to make sure let's do that again.
Okay.
So this is the new test tour.
All right let's remove these here, they are not mandatory,
and I think with this we're good to go.
All right, and here you see the defaults that we set before
so the ratings average for five
and the quantity set to zero, okay.
Now in order to create a new review
we actually need to be logged in as a regular user,
so not an administrator.
So let's get a normal user, let's say.
So let's just use this Laura here.
So Laura@example.com
and as I mentioned earlier the password is always the same.
So let's do that and now we will create new review on tour.
So now let's get the tour that we created earlier,
so this ID and put it here in the URL.
Okay, and so remember that this route
now automatically gets a tour ID from the URL
and a user ID from the currently logged in user,
okay.
So let's use this one as the first rating,
let's send it, and now I want to take a look at our console.
We have an output here, but for some reason it's only
like this empty array.
So something must not be working here.
So let's take a look at the tour ID here just to make sure
that we're getting the right value here, ID
and let's also comment out this part here
just to see if maybe there's some error in this part,
and so let's now try it again.
I'll simply add the same one here again, okay.
Okay, so now we actually get something.
So we have this one review which is
the one that we created initially.
So it starts with this ac15,
but the one that I just created is this 66d, okay.
So let's take a look at that here in Compass.
So we need to go to the last page.
Okay, and so the one that shows up in our console
is only this one here, but not the second one
that we just created, okay.
So if we now create a third one
then we should probably see the first and the second one.
So let's try that.
Okay.
Send this one and now here in our output
I actually get all three of them.
So that doesn't make much sense,
but anyway the point that I wanted to make here
is that we shouldn't use pre,
but instead we should use the post save middleware, okay.
And that's because at pre save
the current review is not really
in the collection just yet.
So therefore, when we then do this match here
it shouldn't be able to then appear in the output here,
because again at this point in time
it's not really saved into the collection just yet, okay.
So it's best to use post here,
because at that time, of course,
all the documents are already saved in the database
and so that's then a great time
to actually do this calculation with all the reviews already
and then store the result on the tour.
Okay, so I'm not really sure
why this one here actually showed up at this point,
but anyway let's now try it with another one.
So that's the fourth one,
and now we get next is not a function and indeed
as I mentioned earlier the post middleware
does not get access to next
and so therefore, we of course cannot call it,
cannot use it.
All right, and so here we have our next rating,
okay.
So it now shows up here as well.
So we don't need that and now we can safely
try to calculate our statistics here.
Give it a save, and actually let's
delete all of these reviews.
So I basically want to start from scratch, okay.
So
all of these five reviews here should go
and now we're ready to go.
So let's actually create this one as the first one
and now let's go ahead and take a look at our statistics
and here we go.
So this is the current tour ID
then the number of ratings is one
and the average is of course three.
So if we add another one with the rating of five
well then the average should be four
and we should have the number of ratings equal to two.
So let's try that and indeed just as I said
two ratings, average of four.
And just another one just to make really sure,
let's add another one with five,
and here we go.
Great.
So we're now correctly calculating the statistics,
but of course they're not yet being persisted
to the current tour document.
So if we come here in Compass
to the tour that we just created,
probably it's the last one here.
Yep, so our test tour, it still has these default values
of 4.5 and zero,
okay.
So now it's time to actually persist
the calculated statistics into this tour document,
okay.
And so let's do that.
First of all, in order to be able to do that
we need to require the tour model, right.
So tour
require
and there we go.
So what we need to do is to basically find
the current tour and then update it.
So we did that before many times, findById
and update,
okay.
And the ID is of course the tour ID that was passed in
into the function and then an object
of the data that we actually want to update.
So let's get these field names from here.
So we have ratingsQuantity
on one hand.
Let's put it here to one for now
just so we don't get an error
and ratingsAverage.
Again, to prevent any errors,
because now I want to come here
and take a look at these statistics.
So you see they are stored in an array actually, okay.
So we need to go to the first position of that array,
which is where this object is.
So stats at position zero
and then from there we get the end rating property,
okay.
So this one here.
And then of course the same for average rating.
Okay.
And now we also need to await it.
So as always this returns a premise and so we can await it.
Now we could also store the result of this to a variable,
but we don't really need the tour at all.
All we want to do is to really update it,
and so we do not store
the results value of the premise anywhere.
Okay.
So let's simply try to add another review here,
this time with one terrible.
Okay.
And first of all, let's take a look at the statistics.
So of course, we get the rating of four,
so the number of ratings, and the average of 3.5.
So now in order to really see if it worked
let's check out Compass or we can also use
or get tour end point,
okay.
So we need the tour ID, that we can copy from here,
paste it here, and now comes the moment
and indeed it worked,
perfect.
So that's awesome, absolutely fantastic.
Just one more time for the fun of it
let's add another review here.
Okay.
Update this one, and so now we get five
and our average went up by a little bit.
Great.
So let's now just take a second and recap what we did here.
So we started by creating a static method.
So this entire function here
to basically create the statistics of the average
and number of ratings for the tour ID
for which the current review was created, okay.
And we created this function as a static method,
because we needed to call the aggregate function
on the model.
So in a static method to this variable
calls exactly to a method.
So it's very handy in these cases.
So we constructed our aggregation pipeline here
where we selected all the reviews that matched
the current tour ID, and then they're calculated,
the statistics for all of these reviews.
Then after that was done
we saved the statistics to the current tour, okay.
Then in order to actually use this function
we call it after a new review has been created, okay.
For that we need to use this.constructor
because this is what points to the current model.
Now keep in mind how we said that we also want to update
the statistics whenever a review is edited or deleted,
because these actions will, of course,
also influence the number and the average.
However, doing so is a bit more complex.
So since this video is already running pretty long
let's actually leave that to the next one.