Query across a JOIN (and Love it)

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

What about a JOIN query with Doctrine? Well, they're really cool.

Here's our last challenge. Go to /genus. Right now, this list is ordered by the speciesCount property. Instead, I want to order by which genus has the most recent note - a column that lives on an entirely different table.

In GenusRepository, the list page uses the query in findAllPublishedOrderedBySize(). Rename that to findAllPublishedOrderedByRecentlyActive():

... lines 1 - 7
class GenusRepository extends EntityRepository
{
... lines 10 - 12
public function findAllPublishedOrderedByRecentlyActive()
{
... lines 15 - 21
}
}

Go change it in GenusController too:

... lines 1 - 12
class GenusController extends Controller
{
... lines 15 - 42
public function listAction()
{
... lines 45 - 46
$genuses = $em->getRepository('AppBundle:Genus')
->findAllPublishedOrderedByRecentlyActive();
... lines 49 - 52
}
... lines 54 - 118
}

Tip

PhpStorm has a great refactoring tool to rename everything automatically. Check out the Refactoring in PhpStorm tutorial.

Adding the Join

Let's go to work! Remove the orderBy line. We need to order by the createdAt field in the genus_note table. And we know from SQL that we can't do that unless we join over to that table. Do that with, ->leftJoin('genus') - because that's the alias we set on line 15 - genus.notes:

... lines 1 - 7
class GenusRepository extends EntityRepository
{
... lines 10 - 12
public function findAllPublishedOrderedByRecentlyActive()
{
return $this->createQueryBuilder('genus')
->andWhere('genus.isPublished = :isPublished')
->setParameter('isPublished', true)
->leftJoin('genus.notes', 'genus_note')
... line 19
->getQuery()
->execute();
}
}

Why notes? This is the property name on Genus that references the relationship. And just by mentioning it, Doctrine has all the info it needs to generate the full JOIN SQL.

Joins and the Inverse Relation

Remember, this is the optional, inverse side of the relationship: we added this for the convenience of being able to say $genus->getNotes():

... lines 1 - 11
class Genus
{
... lines 14 - 45
/**
* @ORM\OneToMany(targetEntity="GenusNote", mappedBy="genus")
* @ORM\OrderBy({"createdAt" = "DESC"})
*/
private $notes;
... lines 51 - 106
/**
* @return ArrayCollection|GenusNote[]
*/
public function getNotes()
{
return $this->notes;
}
}

And this is the second reason you might decide to map the inverse side of the relation: it's required if you're doing a JOIN in this direction.

Tip

Actually, not true! As Stof suggested in the comments on this page, it is possible to query over this join without mapping this side of the relationship, it just takes a little bit more work:

$this->createQueryBuilder('genus')
    // ...
    ->leftJoin(
        'AppBundle:GenusNote',
        'genus_note',
        \Doctrine\ORM\Query\Expr\Join::WITH,
        'genus = genus_note.genus'
    )
    // ...

Back in GenusRepository, give leftJoin() a second argument: genus_note - this is the alias we can use during the rest of the query to reference fields on the joined genus_note table. This allows us to say ->orderBy('genus_note.createdAt', 'DESC'):

... lines 1 - 7
class GenusRepository extends EntityRepository
{
... lines 10 - 12
public function findAllPublishedOrderedByRecentlyActive()
{
return $this->createQueryBuilder('genus')
->andWhere('genus.isPublished = :isPublished')
->setParameter('isPublished', true)
->leftJoin('genus.notes', 'genus_note')
->orderBy('genus_note.createdAt', 'DESC')
->getQuery()
->execute();
}
}

That's it! Same philosophy of SQL joining... but it takes less work.

Head back and refresh! Ok, the order did change. Look at the first one - the top note is from February 15th, the second genus has a note from February 11 and at the bottom, the most recent note is December 21st. I think we got it!

Question: when we added the join, did it change what the query returned? Before, it returned an array of Genus objects... but now, does it also return the joined GenusNote objects? No: a join does not affect what is returned from the query: we're still only selecting from the genus table. There's a lot more about that in our Doctrine Queries tutorial.

Ok, that's it! That's everything - you are truly dangerous with Doctrine now. Sure, there are some more advanced topics - like Doctrine events, inheritance and ManyToMany relations - but we'll save those for another day. Get to work on that project... or keep going with me to learn more Symfony! I promise, more bad jokes - like worse than ever.

See you next time!

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}