OrderBy & fetch EXTRA_LAZY
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIt's great that each article now has real, dynamic comments at the bottom. But... something isn't right: the comments are in a, kind of, random order. Actually, they're just printing out in whatever order they were added to the database. But, that's silly! We need to print the newest comments on top, the oldest at the bottom.
How can we do this? Check out the template. Hmm, all we're doing is calling article.comments
:
// ... lines 1 - 6 | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
// ... lines 11 - 39 | |
<div class="row"> | |
<div class="col-sm-12"> | |
// ... lines 42 - 57 | |
{% for comment in article.comments %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
<img class="comment-img rounded-circle" src="{{ asset('images/alien-profile.png') }}"> | |
<div class="comment-container d-inline-block pl-3 align-top"> | |
<span class="commenter-name">{{ comment.authorName }}</span> | |
<small>about {{ comment.createdAt|ago }}</small> | |
<br> | |
<span class="comment"> {{ comment.content }}</span> | |
<p><a href="#">Reply</a></p> | |
</div> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
// ... lines 79 - 87 |
Which is the getComments()
method on Article
:
// ... lines 1 - 13 | |
class Article | |
{ | |
// ... lines 16 - 170 | |
/** | |
* @return Collection|Comment[] | |
*/ | |
public function getComments(): Collection | |
{ | |
return $this->comments; | |
} | |
// ... lines 178 - 200 | |
} |
The great thing about these relationship shortcut methods is that.... they're easy! The downside is that you don't have a lot of control over what's returned, like, the order of this article's comments.
Well... that's not entirely true. We can control the order, and I'll show you how. Actually, we can control a lot of things - but more on that later.
@ORM\OrderBy()
Scroll all the way to the top and find the comments
property:
// ... lines 1 - 13 | |
class Article | |
{ | |
// ... lines 16 - 60 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article") | |
*/ | |
private $comments; | |
// ... lines 65 - 200 | |
} |
Add a new annotation: @ORM\OrderBy()
with {"createdAt" = "DESC"}
:
// ... lines 1 - 13 | |
class Article | |
{ | |
// ... lines 16 - 60 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article") | |
* @ORM\OrderBy({"createdAt" = "DESC"}) | |
*/ | |
private $comments; | |
// ... lines 66 - 201 | |
} |
That's it! Move over and, refresh! Brilliant! The newest comments are on top. This actually changed how Doctrine queries for the related comments.
Oh, and I want to mention two quick things about the syntax for annotations. First... well... the syntax can sometimes be confusing - where to put curly braces, equal sign etc. Don't sweat it: I still sometimes need to look up the correct syntax in different situations.
Second, for better or worse, annotations only support double quotes. Yep, you simply cannot use single quotes. It just won't work.
Fetch EXTRA_LAZY
I want to show you one other trick. Go back to the homepage. It would be really nice to list the number of comments for each article. No problem! Open homepage.html.twig
. Then, inside the articles loop, right after the title, add a <small>
tag, a set of parentheses, and use {{ article.comments|length }}
and then the word "comments":
// ... lines 1 - 2 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<!-- Article List --> | |
<div class="col-sm-12 col-md-8"> | |
// ... lines 10 - 18 | |
<!-- Supporting Articles --> | |
{% for article in articles %} | |
<div class="article-container my-1"> | |
<a href="{{ path('article_show', {slug: article.slug}) }}"> | |
// ... line 24 | |
<div class="article-title d-inline-block pl-3 align-middle"> | |
// ... line 26 | |
<small>({{ article.comments|length }} comments)</small> | |
// ... lines 28 - 30 | |
</div> | |
</a> | |
</div> | |
{% endfor %} | |
</div> | |
// ... lines 36 - 55 | |
</div> | |
</div> | |
{% endblock %} |
I love it! Refresh the homepage. It works effortlessly! But... check out the queries down here on the web debug toolbar. If you click into it, there are suddenly 6 queries! The first query is what we expect: it finds all published articles.
The second query selects all of the comments
for an article whose id is 176. The next is an identical query for article 177. As we loop over the articles, each time we call getComments()
, at that moment, Doctrine fetches all of the comments for that specific Article
. Then, it counts them.
This is a classic, potential performance issue with ORM's like Doctrine. It's called the N+1 problem. And, we'll talk about it later. But, it's basically that the cool lazy-loading of relationships can lead to an extra query per row. And this may cause performance issues.
But, forget about that for now, because, there's a simpler performance problem. We're querying for all of the comments for each article... simply to count them! That's insane!
This is the default behavior of Doctrine: as soon as you call getComments()
and use that data, it makes a query at that moment to get all of the comment data, even if you eventually only need to count that data.
But, we can control this. In Article,
at the end of the OneToMany
annotation, add fetch="EXTRA_LAZY"
:
// ... lines 1 - 13 | |
class Article | |
{ | |
// ... lines 16 - 60 | |
/** | |
* @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="article", fetch="EXTRA_LAZY") | |
* @ORM\OrderBy({"createdAt" = "DESC"}) | |
*/ | |
private $comments; | |
// ... lines 66 - 201 | |
} |
Now, go back to the page and refresh. We still have six queries, but go look at them. Awesome! Instead of selecting all of the comment data, they are super-fast COUNT queries!
Here's how this works: if you set fetch="EXTRA_LAZY"
, and you simply count the result of $article->getComments()
, then instead of querying for all of the comments, Doctrine does a quick COUNT query.
Awesome, right! You might think that it's so awesome that this should always be the way it works! But, there is one situation where this is not ideal. And actually, we have it! Go to the article show page.
Here, we count the comments first, and then we loop over them:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<div class="show-article-container p-3 mt-4"> | |
// ... lines 11 - 39 | |
<div class="row"> | |
<div class="col-sm-12"> | |
<h3><i class="pr-3 fa fa-comment"></i>{{ article.comments|length }} Comments</h3> | |
// ... lines 43 - 57 | |
{% for comment in article.comments %} | |
// ... lines 59 - 70 | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 81 - 87 |
Look at the profiler now. Thanks to EXTRA_LAZY
, we have an extra query! It counts the comments... but then, right after, it queries for all of them anyways. Before we were using EXTRA_LAZY
, this count query didn't exist.
So, sorry people, like life, everything is a trade-off. But, it's still probably a net-win for us. But as always, don't prematurely optimize. Deploy first, identify performance issues, and then solve them.
Hi, I wanna start by saying that I really enjoy this series and the way you teach.
Now, I come from a no-framework background and I would build all my queries by hand.
I absolutely want to start using Symfony, but the way it handles the database is very confusing and strange for me because I am used to build and control all my queries.
For example, I don't know if this will be covered in the next videos, but I wanted to know how to eliminate the 5 extra queries to count the comments on the home page. Is true that with EXTRA_LAZY is a little faster than just getting all comments for each article, but you still make a query for each article.
If I were to build this page how I do normally, I would just join the articles to the comments table when fetching all articles, and do a count on the joined comments table, so the number of comments of each article will be a field/property on each article and there will only be one query on the home page.
So my question is, can we control the fetch all articles method to do this, or I have to build a custom method and use that for fetching articles?
Thank you.