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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThere'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.
I have two related questions question:
I ask these questions because I have created an entity that describes a document layout for a specific user. I have created two ManyToOne relations inside the layout entity that relates to the user entity and to the document entity.
When I delete a document in the database, I get an SQL error about the document, which can not be deleted. Should I have defined a OneToMany relation in the user and document entities that relate them to a layout entity?
I am quite puzzled