Doctrine Relationships for Beginners


Finishing up our introduction to Doctrine in Symfony 2.7 we are going to look at Doctrine's relationships.

You can't go far with SQL databases without coming across the topic of relationships. I'm going to assume you know the basics of what these terms mean and instead, focus on how we can use Doctrine annotations inside our Entities to relate 'things' together.

The handy reference for all the available entity associations (or relationships) can be found on the official Doctrine documentation under Chapter 5 Association Mapping.

Gotcha

If you are using the official Doctrine documentation chapter for future reference, make sure you add in the ORM\ before using any of the examples, e.g.:

    /**
     * @ManyToOne(targetEntity="Address")
     * @JoinColumn(name="address_id", referencedColumnName="id")
     **/
    private $address;

Becomes:

    /**
     * @ORM\ManyToOne(targetEntity="Address")
     * @ORM\JoinColumn(name="address_id", referencedColumnName="id")
     **/
    private $address;

Also, make sure you have the correct use statement at the top of your class definition:

use Doctrine\ORM\Mapping as ORM;

Target Entity, Join Column, uh huh, uh huh?

I still remember when I first encountered Doctrine. I needed to set up some relationships for some tables I had already been using in the project before I'd migrated to using Symfony 2 as the framework.

In plain old SQL I knew exactly what I was doing. But in Doctrine, I suddenly had to understand a bunch of new, weird sounding terms.

Each of the relationships described in the Doctrine docs are fairly self explanatory. What I found confusing was the annotation terminology. Let's look at a few different examples:

// src/AppBundle/Entity/BlogPost.php
/** @ORM\Entity **/
class BlogPost
{
    /**
     * @ORM\ManyToOne(targetEntity="Category")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     **/
    private $category;

targetEntity="Category" - we need to tell Doctrine which other Entity we are associating with.

You can use the full namespace path to the entity we are wanting to link our BlogPost to (the target), but if you Category entity is in the same namespace as your BlogPost entity, we can just give the entity name instead.

ORM\JoinColumn - we are wanting to relate or join this entity (think: table) with another entity. We need to tell Doctrine how to do this.

This will create a new data column inside our SQL table for us.

name="category_id" - much like when naming a table, or a field inside our Entity class, we give this new column a name. Convention is to call it whatever you are linking too. This is essentially the foreign key.

As such, we are saying here that our $category variable will contain a number which represents the ID of a Category (think: the id of a row in the category table).

referencedColumnName="id" - the column in the other table we are wanting to link with. This is the id 99.9% of the time.

Ultimately we would end up with our underlying SQL data table containing a column called category_id, and assuming we were linking this BlogPost to Category id: 4, then the category_id column for this BlogPost would have the number 4 in it.

Unidirectional, Bidirectional, and Self-referencing

Another somewhat confusing step is understanding what is meant by the terms Unidirectional, Bidirectional, and Self-referencing on the Doctrine documentation.

These terms are specific to Doctrine, but are worth covering for clarity:

Unidirectional

A unidirectional relationship is one where one thing owns another thing or things, but the owned things don't know they are owned.

An example of this might be a Search Engine Result Page (SERP) contains links to 10 different pages.

Let's say we searched Google for 'Symfony 2'. On page one there would be the 10 results. Let's say Symfony.com is #1.

Our SERP contains ten links.

If we were to grab the first link in that collection of links - Symfony.com - it wouldn't know that it was owned by the SERP for the keyword of 'Symfony 2'.

This relationship is unidirectional. The SERP cares about its links. The individual links don't know or care which SERP they appear in.

The reason for doing this is that it is much easier to reason about and manage data that can only flow one way.

Bidirectional

A bidirectional relationship is one where both sides need to know about the other.

ManyToOne is always the owning side of a bidirectional association.

OneToMany is always the inverse side of a bidirectional association.

Think of a BlogPost and Tags.

We want our BlogPost to have many tags. Our BlogPost cares about those Tags.

From the opposite side, we would quite like a tag cloud widget. If we were to pull out a Tag from our database called 'fly fishing' we would want to know all the associated BlogPost entities that had that Tag.

We need to look up the data from sides.

This is harder to manage, and involves choosing one side that owns the relationship. One side is the owning side, and the other is the inverse side. We will cover this below.

Self Referencing

The least used of the three. A self-referencing relationship is one where a thing is related to other objects (things) of the same type.

A Category may contain sub-categories, which are objects / entities of type Category in their own right.

You don't want to end up building a Category entity, and a SubCategory entity, and then a SubSubCategory entity. And then your boss comes in and says actually, Category needs moving down one, so you have to add in ParentCategory entity... good lord oh my no.

Inversed By and Mapped By

This example is taken (almost) directly from the One-To-Many, Bidirectional section in the docs.

I say almost, because I have added in the @ORM\ bit.

In this example, Feature is the owning side, and Product is the inverse side.

This may seem unusual, so be sure to read the rules of Doctrine's bidirectional associations if unclear.

// src/AppBundle/Entity/Product.php
<?php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/** @ORM\Entity **/
class Product
{
    // ...
    /**
     * @ORM\OneToMany(targetEntity="Feature", mappedBy="product")
     **/
    private $features;
    // ...

    public function __construct() {
        $this->features = new ArrayCollection();
    }
}

and in src/AppBundle/Entity/Feature.php:

/** @ORM\Entity **/
class Feature
{
    // ...
    /**
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="features")
     * @ORM\JoinColumn(name="product_id", referencedColumnName="id")
     **/
    private $product;
    // ...
}

We've already covered most of these annotations, but there's a couple of new things going on here.

Inversed Side of the Relationship

In the real world, when you are working on a new codebase, or are in the process of learning Symfony and Doctrine, it is not always immediately obvious which side is the owning side, and which is the inverse side.

By looking at the annotations, we can understand the underlying relationships.

mappedBy="product" - this is a quick way to determine which is the owning side and which is the inverse side of a Doctrine relationship.

My mind always confuses the two, so it's worth writing this down for reference.

mappedBy means this is pointing to the inverse side of the relationship.

Remember:

OneToMany is always the inverse side of a bidirectional association.

The attribute - in this case "product" is the name of the property that lives on the owning side - in this case the $product property.

Owning Side of the Relationship

inversedBy="features" - this indicates that this is the owning side of the relationship.

The obvious confusion is you would think to use inversedBy on the owning side, not the inverse side which would be seemingly more intuitive. Likewise, the mappedBy attribute doesn't really mean much at first glance. At least, it didn't to me.

Similarly to the mappedBy attribute, we pass in the name of the property that this relates to in the other entity.

inversedBy="features" therefore maps to the $features property.

ManyToOne is always the owning side of a bidirectional association.

Why is the 'many' side the owning side? Because it holds the foreign key.

The OneToMany side of a relation is inverse by default, since the foreign key is saved on the Many side.

Why is this important?

Doctrine will only check the owning side of an association for changes.

If you make changes only on the inverse side of the association then Doctrine will ignore these changes. When telling Doctrine to update / save, you must do this with the Entity that is the owning side of the relationship.

More info here.

Collections

The last thing to cover here is why we sometimes use a Collection.

You will likely have noticed, both in the last example, and in the examples on the Doctrine Association Mapping documentation that sometimes we new up an ArrayCollection.

    public function __construct() {
        $this->comments = new \Doctrine\Common\Collections\ArrayCollection();
    }

I have covered Collections in another tutorial series. They are very powerful and don't just have to be used with Doctrine data.

Any time we have a relationship where one side can have many things associated with it, we need a Collection to hold those 'things'.

Imagine we have a Blog Post that has several Comments.

One blog post > many comments.

When we pull out the Blog Post from the database we would also like all the associated comments so we can loop over them and display them.

We are dealing with objects here so we need a way to store all these Comment objects / entities.

The ArrayCollection is a set of helpful methods that wrap over the top of a plain old PHP array. The Collection interface defines ~20 methods that make working with collections of things much easier.

As such, whilst behind the scenes our objects are inside a standard PHP array, by using the ArrayCollection we can work with our array in an object oriented manner.

We create a new ArrayCollection() in the constructor because even if this Blog Post were to be brand new, it would need a container ready and available so things don't blow up in our face if we didn't have any comments, yet we wanted to render this blog post in a Twig template.

That's just an example. Think of it as a precautionary measure so that we can interact with objects that have relations (a Blog Post with some Comments) and objects that don't have any relations (a brand new Blog Post with no Comments) in exactly the same way.

If we tried to loop over our Comments and display them without at least having an empty Collection on new Blog Posts, the first post with comments would work just fine. But the second post without comments would try to access a none-existent variable (our undefined $this->comments) and fail miserably.

Further Viewing

Interested in knowing some of the more advanced ways you can use Doctrine? This tutorial series on Doctrine Performance Optimisations will show you a few tips and tricks to get more bang for your buck.

Useful Tools

If you have been following along with the videos and are using SQLite, a decent client from my experience has been the DB Browser for SQLite. Mac, windows, Linux, and even FreeBSD clients are available. Excellent.

Episodes