‫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.