Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Owning Vs Inverse Sides of a Relation

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

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

There's a, kind of, complex topic in Doctrine relations that we need to talk about. It's the "owning versus inverse side" of a relationship.

We already know that any relation can be seen from two different sides: Question is a OneToMany to Answer...

... lines 1 - 14
class Question
{
... lines 17 - 51
/**
* @ORM\OneToMany(targetEntity=Answer::class, mappedBy="question")
*/
private $answers;
... lines 56 - 176
}

and that same relation can be seen as an Answer that is ManyToOne to Question.

... lines 1 - 11
class Answer
{
... lines 14 - 37
/**
* @ORM\ManyToOne(targetEntity=Question::class, inversedBy="answers")
* @ORM\JoinColumn(nullable=false)
*/
private $question;
... lines 43 - 95
}

So... what's the big deal? We already know that we can read data from both sides: we can say $answer->getQuestion() and we can also say $question->getAnswers().

Setting the Other Side of the Relation

But can you set data from both sides? In AnswerFactory, when we originally started playing with this relationship, we proved that you can say $answer->setQuestion() and Doctrine does correctly save that to the database.

Now let's try the other direction. I'm going to paste in some plain PHP code to play with. This uses the QuestionFactory to create one Question - I'm using it because I'm kinda lazy - and then creates two Answer objects by hand and persists them. We don't need to persist the Question because the QuestionFactory saves it entirely.

... lines 1 - 4
use App\Entity\Answer;
use App\Entity\Question;
... lines 7 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 16 - 29
$question = QuestionFactory::createOne();
$answer1 = new Answer();
$answer1->setContent('answer 1');
$answer1->setUsername('weaverryan');
$answer2 = new Answer();
$answer2->setContent('answer 1');
$answer2->setUsername('weaverryan');
$manager->persist($answer1);
$manager->persist($answer2);
... lines 40 - 41
}
}

At this point, the Question and these two answers are not related to each other. So, not surprisingly, if we run:

symfony console doctrine:fixtures:load

we get our favorite error: the question_id column cannot be null on the answer table. Cool! Let's relate them! But this time, instead of saying, $answer1->setQuestion(), do it with $question->addAnswer($answer1)... and $question->addAnswer($answer2).

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 16 - 29
$question = QuestionFactory::createOne();
$answer1 = new Answer();
... lines 32 - 33
$answer2 = new Answer();
... lines 35 - 36
$question->addAnswer($answer1);
$question->addAnswer($answer2);
... lines 41 - 44
}
}

If you think about it... this is really saying the same thing as when we set the relationship from the other direction: this Question has these two answers.

Let's see if it saves! Run the fixtures:

symfony console doctrine:fixtures:load

And... no errors! I think it worked! Double-check with:

SELECT * FROM answer

symfony console doctrine:query:sql 'SELECT * FROM answer'

Let's see... yea! Here are the new answers. Oh, apparently I called them both "answer 1" - silly Ryan. But more importantly, each answer is correctly related to a Question.

Ok! so it turns out you can set data from both sides. The two sides of the relationship apparently behave identically.

Now, at this point, you might be saying to yourself:

Why is this guy taking so much time to show me that something works exactly like I expect it too?

The "setters" Synchronize the Other Side of the Relation

Great question! Because... this doesn't really work like we just saw. Let me show you.

Open the Question class and find the addAnswer() method.

... lines 1 - 14
class Question
{
... lines 17 - 155
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
$answer->setQuestion($this);
}
return $this;
}
... lines 165 - 176
}

This was generated for us by the make:entity command. It first checks to see if the $answers property already contains this answer.... just to avoid a duplication. If it does not, it, of course, adds it to that property. But it also does something else, something very important: $answer->setQuestion($this). Yup, it sets the other side of the relation.

So if an Answer is added to a Question, that Question is also set onto that Answer. Now, watch what happens if we comment-out this line...

... lines 1 - 14
class Question
{
... lines 17 - 155
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
//$answer->setQuestion($this);
}
return $this;
}
... lines 165 - 176
}

and then go reload the fixtures:

symfony console doctrine:fixtures:load

An error! The question_id column cannot be null on the answer table! It did not relate the Question to the Answer properly!

Owning vs Inverse

This is what I wanted to talk about. Each relation has two different sides and these sides have a name: the owning side and the inverse side. For a ManyToOne and OneToMany relationship, the owning side is always the ManyToOne side. And it's easy to remember: the owning side is where the foreign key column lives in the database. In this case, the answer table will have a question_id column so this is the "owning" side.

The OneToMany side is called the inverse side.

Why is this important? It's important because, when Doctrine saves an entity, it only looks at the data on the owning side of a relationship. Yup, it looks at the $question property on the Answer entity to figure out what to save to the database. It completely ignores the data on the inverse side. Really, the inverse side exists solely for the convenience of us reading that data: the convenience of being able to say $question->getAnswers().

So right now, we are only setting the inverse side of the relationship. And so, when it saves the Answer, it does not link the Answer to this Question.

Inverse Side is Optional

And actually, the inverse side of a relationship is entirely optional. The make:entity command asked us if we wanted to map this side of the relationship. We could delete everything inside of Question that's related to answers, and the relationship would still be set up in the database and we could still use it. We just wouldn't be able to say $question->getAnswers().

I'm telling you all this so that you can avoid potential WTF moments if you relate two objects... but they mysteriously don't save. Fortunately, the make:entity command takes care of all this ugliness for us by generating really smart addAnswer() and removeAnswer() methods that synchronize the owning side of the relationship. So unless you don't use make:entity or start deleting code, you won't need to think about this problem on a day-to-day basis.

Put back the $answer->setQuestion() code so that we can, once again, safely set the data from either side.

... lines 1 - 14
class Question
{
... lines 17 - 155
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
$answer->setQuestion($this);
}
return $this;
}
... lines 165 - 176
}

Back in the fixtures, now that we've learned all of this, delete the custom code.

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
$questions = QuestionFactory::createMany(20);
QuestionFactory::new()
->unpublished()
->many(5)
->create()
;
AnswerFactory::createMany(100, function() use ($questions) {
return [
'question' => $questions[array_rand($questions)]
];
});
$manager->flush();
}
}

And then, let's reload our fixtures:

symfony console doctrine:fixtures:load

Next: when we call $question->getAnswers()... which we're currently doing inside of our template, what order is it returning those answers? And can we control that order? Plus we'll learn a config trick to optimize the query that's made when all we need to do is count the number of items in a relationship.

Leave a comment!

9
Login or Register to join the conversation
Yuki-K Avatar

Hi

I was wondering why, when I close the terminal (stop the mysql container inside docker) and hit docker compose up again, the datas remain in the project.
I thought database holds the datas, but where the datas go when I close my terminal and stop the mysql container?
Is it because stop the container doesn't mean delete and databases and recreate them, but instead
changing the port number to the same database?

docker makes me headache, Help me! lol

Cheers!

Reply

Hey @Yuki-K

woh that's complex question, by default if you didn't configured to store data on your local drive iit stores it inside the container, and it keeps data saved until you didn't destroyed it. But if you want a persisted data you should map you local filesystem with container filesystem so in this case you will not lose any data even if container will be dropped.

Cheers!

1 Reply
PeterM Avatar

The question asks us to pick the answer that is incorrect, but they're all correct.

Reply

Hey Peter,

Yes, this is a tricky question, and that's why the right option is the one that says [SPOILER ALERT] "All of these statements are true"

Cheers!

Reply
PeterM Avatar

But the question is for what is is not true. So by selecting that answer you're saying it's not true that all of the statements are true, i.e. they're all false.

Reply

Ohh I think I got your point. Do you think if we reword the option to say something like "None, all of these statements are correct" would make more sense?

Reply
VERMON Avatar

Hi, just to point out that there is no more subtitle from this video, then re-appear on the 11th: "Collection Criteria for Custom Relation Queries"

Reply

Thanks for pointing that out - we'll get it fixed up - we had a hiccup with our subtitling system and we need to reprocess a couple of them!

1 Reply
VERMON Avatar

You're welcome. Thanks for your reactivity !

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.3
        "doctrine/doctrine-bundle": "^2.1", // 2.4.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.9.5
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.7
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.3.*", // v5.3.7
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/validator": "5.3.*", // v5.3.14
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.33.0
        "symfony/var-dumper": "5.3.*", // v5.3.7
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.5
        "zenstruck/foundry": "^1.1" // v1.13.1
    }
}
userVoice