Loading Like Counts0:00
So, yeah, we want to show likes on the front end. How many likes does a Post have? How many likes does a Comment have? And it's actually fairly simple, at least in its most basic form. Let's jump into our IDE, and I'm going to head into the Post model itself. And seeing as we always want to know how many likes a Post has so that we can show it at any point in the UI, I'm going to use a little property called withCount. And the withCount property will essentially say, look, for any of these relationships, always load a count into this model. So in this case, we want to always load the number of likes into this model whenever we ask for a Post.
always load a count into this model. So in this case, we want to always load the number of likes into this model whenever we ask for a Post. With that done, we can jump into the post resource. And why don't we add just under HTML here a new attribute. We'll call that likes_count. And we'll set that to this.likes_count, which is a column that's automatically generated for us by that with property. So now on the front end, if we open up view dev tools, and let's take a look in show. Now, we should have the post here. And sure enough, likes_count is set to one because our primary user, Luke, has indeed liked that one post. Let's increase the number of likes just to make sure that's actually what's going on.
Seeding Likes for Testing1:19
And sure enough, likes count is set to one because our primary User, Luke, has indeed liked that one Post. Let's increase the number of likes just to make sure that's actually what's going on. We'll say php artisan tinker. Let's grab the first Post. $post equals Post::find(1). And then we can say like factory. We'll create 100 likes. 100 likes. And we need to say that it's for that post, but it won't be enough information just to pass the post.
100 likes. And we need to say that it's for that Post, but it won't be enough information just to pass the Post. We also need to pass the relationship, which is likable, and then we'll call create. There we go. And now if we come back and refresh, we should have not one like, but 101 likes. Brilliant. Let's get that displayed on the front end itself. So if we come into our little show component for Posts, perhaps here we could have a little div. Let's give it a little bit of a margin, mt-4, and a span. And we want to show the number of likes.
Let's give it a little bit of a margin, mt-4, and a span. And we want to show the number of likes. So post.likes count. And, of course, we'll suffix it with likes. There we go. Let's give this a little bit of a nice color. class equals maybe text-pink-500. And we'll make it bold as well. So font-bold. See what that looks like on the front end.
So font bold. See what that looks like on the front end. There we go. 101 likes. And, of course, we'll expand on that as we add support for liking and unliking the Post. So there we go. As I said, nice and simple. I know that I've spent this entire series saying that you shouldn't optimize too soon, but I want to optimize a little soon. And the reason is that, turns out, if you use count on something that has many,
Load Testing Performance2:52
but I want to optimize a little soon. And the reason is that, turns out, if you use count on something that has many, many related models, you can affect application performance. I'm not talking about 100 models here. I'm talking about thousands, hundreds of thousands of models even. So let's do a little bit of load testing so that I can show you what I mean. The current response time we're receiving for this page is 30 milliseconds. And when I'm not recording, it's more like 15 milliseconds. So let's go into our terminal and we'll create a seeder that we can use to create many likes for this post. php artisan make:seeder
So let's go into our terminal and we'll create a seeder that we can use to create many likes for this post. php artisan make:seeder. We'll call it the LikeLoadTestSeeder. That's absolutely fine. Let's open it up in the IDE. And first of all, we need to grab that post. So $post = Post::find, and we're looking for the first post available. And then we want to create maybe, I don't know, 500,000 likes for this post. So you might go for something like Like::factory()->for($post, 'likeable')->count(500000).
So you might go for something like factory for Post, and we call it likeable, and we'll add a count of 500,000. The problem is you're actually going to run out of memory. So if you try to run this, unless you bump up your php memory limit, it will crash halfway through. Instead of increasing our memory limit, I'm going to use a lazy collection. So let's say lazyCollection times, and let's do chunks of 100. So that would be 5,000, and then we'll call the each method, which receives a closure. We don't need the item because it's just going to be a number, but we will pull in the post. And then inside this closure, we're going to create 100 likes. So like factory 100 for the post.
And then inside this closure, we're going to create 100 likes. So like factory 100 for the post. And again, we need to pass that likeable relationship, and then we'll call create. Of course, it would be nice to see how far along the process we are when we run this seeder. So we can use a progress bar from Laravel prompts to do that. We'll say progress equals progress. The label can be creating likes. We need to give it a number of steps. Let's set that to 500,000, seeing as that's how many likes we're going to create inside this load tester. Then we need to start the progress indicator.
Let's set that to 500,000, seeing as that's how many likes we're going to create inside this load tester. Then we need to start the progress indicator. So that is progress start. After we finish creating the likes, we need to finish the progress bar, which is progress finish. And then inside here, we'll need access to progress. And every time we create 100 likes, we can advance the progress bar. So progress advance by 100 to match the number of likes that we're creating. I think that that will work, and because it's lazy, we won't run into memory issues. So from the terminal, let's go ahead and say php artisan db:seed, and we'll specify that we want to run the LikeLoadTestSeeder.
So from the terminal, let's go ahead and say php artisan db:seed, and we'll specify that we want to run the LikeSeeder. And right away, we've already run into an exception. What is it? Integrity constraint violation, duplicate entry. Okay, so we have these unique email addresses that we're using for our Users, but the problem is that Faker only has so many unique email addresses. So to address this, let's go into the LikeFactory. And let's say that when we create a User for a Like, we're going to give it a slightly different email address.
And let's say that when we create a User for a like, we're going to give it a slightly different email address. And the email address can be something like test+, and then let's just use a string UUID. So string UUID test+@example.com. So this will give us a very unique email. Obviously, every UUID you create is completely unique. Seems like a waste of UUIDs, but it'll work. It'll get the job done for what we're interested in. Let's go back to the terminal and try to run this again. And there we go.
Let's go back to the terminal and try to run this again. And there we go. It's creating likes, and you can see that progress indicator moving as it goes. If we go back to the front end as this is running and I refresh, yeah, you can see the number of likes there jumping up every single second. So we'll give our seeder a moment to finish, and then I'll be back to talk to you about performance. And it's finally finished. If we go to the front end, yeah, we now have 500,000 likes on this post. But take another look at the network panel.
If we go to the front end, yeah, we now have 500,000 likes on this Post. But take another look at the network panel. Remember the time before was 30 milliseconds? We're now up to 106 milliseconds to load this page. It's having to count so many related likes that it is slowing down the page considerably, three times slower than before. Now, maybe that's not an issue for you. Maybe that's not something you want to worry about. But I just want to discuss an alternative approach to this that will help us avoid those performance issues.
Denormalizing Like Counts7:36
But I just want to discuss an alternative approach to this that will help us avoid those performance issues. And that is database denormalization. At the moment, obviously, as the number of likes increases or decreases, we automatically calculate the count by doing a SQL query. But instead, we could store the number of likes on the post table or the comments table itself, and we could manually increment or decrement that number every time a new like comes in. So let's try it. On the post table, we'll add an unsigned big integer called likesCount.
So let's try it. On the post table, we'll add an unsigned big integer called likesCount. And we could set the default here to zero. We'll also do the same on the comments table. Head down to comments. Yep, here we go, table, unsigned big integer. We'll say likesCount, and then we'll set the default to zero again. Inside our Post model, we can get rid of this withCount eager load. And then also on our PostResource, we don't actually have to change anything because the column name that we've created is the same as what would be created.
And then also on our Post resource, we don't actually have to change anything because the column name that we've created is the same as what would be created if we eager loaded the likes. Of course, we could add this to our Comment resource as well while we're thinking on it. There we go, likesCount has been added. And then we'll go ahead and refresh our database. So php artisan migrate:fresh --seed, let that run. And on the front end, I would expect that we'll be back down to zero likes. We might actually already have a like in the database for this post,
And on the front end, I would expect that we'll be back down to zero likes. We might actually already have a like in the database for this Post, but it's not going to show because now we are in charge of incrementing or decrementing this value. Now, one thing I'm going to do inside our Like load test seeder, once we've finished creating all the likes for this Post, is I'll manually increment that likesCount. So we'll say post. There's a cool little method on Eloquent Models called increment where we can say that we want to add to a column.
There's a cool little method on Eloquent Models called increment where we can say that we want to add to a column. Well, it will be the likesCount column, and we'll set the value to 500,000. And then when we run this again, essentially, it will do the same thing. But after it's finished, it's also going to change this column here. So once more, I'm going to go ahead and run that seeder, php artisan db:seed --class=LoadTestSeeder. So while that's running, let's just talk about this again. We're moving from a normalized database structure where the database is in charge of counting
We're moving from a normalized database structure where the database is in charge of counting how many likes a post or comment has to a denormalized database structure where, instead, we pull the number of likes from a simple integer column on the posts or comments table. That does mean that, as developers, we are taking on the responsibility of keeping those values up to date. So in the like controller that we'll be creating in an upcoming episode, we'll have to increment or decrement that value. But again, I think that it's worth doing that.
we'll have to increment or decrement that value. But again, I think that it's worth doing that because the performance benefit we gain is measurable. It will stop our application slowing down over time. There we go, finished. Let's go back to the browser. 500,000 likes on this post, and it took 26 milliseconds. So there we go. We solved the performance scaling issue. Doesn't matter how many likes we have on a post,
Formatting Like Numbers10:56
We solved the performance scaling issue. Doesn't matter how many likes we have on a Post, it will always take the same amount of time to load because we've denormalized our database. One thing I'm not too keen on is the formatting of this. I think we can make it more friendly for the user. Let's go into our PostResource, and Laravel has a cool little number helper, Illuminate\Support\Str, where we can abbreviate any given number. And if we come back to the front end and refresh,
where we can abbreviate any given number. And if we come back to the front end and refresh, see we now go to 500k likes or 500,000 likes. So the same output, but it's just a little more friendly to look at. You might also want to play around with the forHumans helper, which will say literally 500,000 likes. But yeah, I prefer the little k abbreviation, so I'm going to stick to the abbreviate method. Let's make sure we also copy that over to the comment.
so I'm going to stick to the abbreviate method. Let's make sure we also copy that over to the Comment resource. Here we are, number, and we're looking for that abbreviate method again. And then let's go ahead and make sure we update our comment view component to also show the number of likes. We could probably add it just to the end of this line here for now.
We could probably add it just to the end of this line here for now. So we'll separate with a pipe character, and we'll say comment.likesCount, and then we'll want to suffix with the word likes. And let's add a little bit of coloring, so class equals text-pink-500. There we go, nice and straightforward, zero likes. Obviously, there are no likes on any of these comments at the moment, but we are now showing them.
Preparing Like/Unlike Feature12:23
Obviously, there are no likes on any of these comments at the moment, but we are now showing them. Great, so we've denormalized the database. We have the ability to actually show the number of likes on the front end, but there is no way for us as a user to like or unlike a Post or Comment. Let's tackle that in the next episode.
