Object relations in Extbase
Domain objects are rarely isolated. A
Conference is held at a
Location, collects many
Comment objects, and is staffed by many
Speaker objects. Extbase maps these relations onto the relational
database and loads the related objects for you when you access the property — no
JOIN in your own code.
From the model's point of view a relation is just a property, and Extbase offers exactly two shapes for it:
- a property that holds one other object — a direct, typed property; or
- a property that holds many objects — an
\TYPO3\.CMS\ Extbase\ Persistence\ Object Storage
This page starts from those two shapes — what you write and how each behaves — then goes behind the scenes to the TCA that drives them, explains why "cardinality" (1:1, 1:n, …) is a property of both sides of a relation rather than of a single field, and finishes with how related objects are loaded and the lazy-loading performance trade-off.
How to declare relations in a model — the attributes, the
Object getter/setter pattern — is covered with the model:
See also
Modelling relations in Extbase — declaring relation properties, lazy and cascade attributes, ObjectStorage accessors.
On this page
A relation to one other object
The simplest relation points from one object to a single other one — a
Conference is held at a
Location. In the model this is a single,
typed, usually nullable property:
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
class Conference extends AbstractEntity
{
protected ?Location $location = null;
protected function getLocation(): ?Location
{
return $this->location;
}
}
The property is nullable because, from this object's side, the relation is optional: a conference may or may not have a location set. Reading it is a plain getter method.
In the database, the relation is stored as a single column on the owning
table that holds the UID of the related record (for example a
location
column on the conference table). The related record does not know it is being
pointed at — nothing on the
Location side records the conference.
Note
This is a relation to at most one other object (0..1). It is tempting to call it a "1:1 relation", but that is not what the model states: nothing here says a location belongs to exactly one conference, or that it knows about the conference at all. The cardinality language (1:1, 1:n, …) only becomes meaningful once both sides are declared — see Behind the scenes: TCA, unidirectional and bidirectional relations.
A relation to many objects
When an object holds a collection of others, the property is an
Object — Extbase's container for
related objects. A
Conference collects many
Comment objects, and is
staffed by many
Speaker objects; both are
Object properties:
<?php
namespace MyVendor\MyExtension\Domain\Model;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
class Conference extends AbstractEntity
{
/** @var ObjectStorage<Comment> */
protected ObjectStorage $comments;
/** @var ObjectStorage<Speaker> */
protected ObjectStorage $speakers;
public function __construct()
{
$this->initializeObject();
}
public function initializeObject(): void
{
$this->comments = new ObjectStorage();
$this->speakers = new ObjectStorage();
}
/** @return ObjectStorage<Comment> */
public function getComments(): ObjectStorage
{
return $this->comments;
}
public function addComment(Comment $comment): void
{
$this->comments->attach($comment);
}
public function removeComment(Comment $comment): void
{
$this->comments->detach($comment);
}
}
You iterate the storage to read the related objects, and add or remove single
objects through dedicated
add /
remove methods
that call
attach and
detach. The
@var
Object annotation is required so that IDEs and static analysis
know the element type.
The two collections above look identical in PHP, but they are stored differently in the database — and that difference decides what happens when you delete the owning object.
Children that belong to the parent
The comments of a conference exist only as part of that conference: a comment
has no life of its own. The natural way to store this is a back-reference
column on the child table — a
conference column on the comment table
holding the parent UID, so no intermediate table is needed. The most common TCA
design for such relations is
type="inline" with a
foreign_ naming that back-reference column. Other field types can
express the same relation, but inline is the usual and most convenient choice
for parent-owned children.
Because the child belongs to the parent, this is the natural place for
#: deleting the conference deletes its comments. See
Modelling relations
for the attribute.
Behind the scenes: TCA, unidirectional and bidirectional relations
The model property tells you that there is a relation and whether it holds one
object or many. It does not, on its own, tell you the full shape of the
relation. That comes from the TCA of the
column: whether there is a back-reference field, an MM table, a
maxitems limit, or an explicit
relationship setting. Extbase reads
that TCA and maps each column to one of its internal relation kinds — a single
related object, a collection with a back-reference, or a collection through an MM
table.
The examples above are all unidirectional: you can navigate from the
conference to its location, comments and speakers, but not back. A
Location usually does not know which conference is held there; a
Speaker
does not list the conferences they speak at. Each relation is declared once, on
the owning side only.
This is why the classic cardinality labels do not apply to a single property. Terms like 1:1, 1:n, n:1 and n:m describe how both ends of a relation are constrained — and you only know both ends when the relation is bidirectional, declared on each side:
Conference→Locationon its own is a relation to one other object. Whether it is "1:1" (each location used by exactly one conference) or "n:1" (many conferences share a location) depends on a constraint that lives on theLocationside — and nothing in the conference model states it.- Likewise a relation to many becomes a "1:n" only if the child side is constrained to a single parent. As written, the comment relation simply collects children; it does not, by itself, forbid a comment from being attached elsewhere.
For everyday extension code this rarely matters, because Extbase relations are overwhelmingly unidirectional — you model the direction you actually navigate and leave the other side unaware. Reach for a bidirectional relation only when you genuinely need to navigate and query from both ends; it means declaring and maintaining the relation, and its TCA, on both models.
Tip
When you do need cardinality made explicit in TCA, the
relationship key
(
'one,
'many,
'one)
declares it on the field. Extbase honours it when deciding whether a column
is a single relation or a collection.
Lazy loading and the N+1 query trap
Eager loading is convenient but not free. Loading a list of 50 conferences, each
with an eagerly loaded
Object of comments, means Extbase loads
every comment of every conference up front — even if the list view never displays
them.
Marking the relation
# defers loading: Extbase installs a
lightweight placeholder and loads the related objects only on first access. A
single relation receives a
\TYPO3\; an
Object becomes a
\TYPO3\. Both resolve on
first access.
Lazy loading solves the load-everything-up-front problem but introduces its opposite — the N+1 query problem. Consider a template that does need the relation for every row:
<f:for each="{conferences}" as="conference">
<h2>{conference.title}</h2>
<p>{conference.comments -> f:count()} comments</p>
</f:for>
If this were a lazy relation, it would run one query to fetch 50 conferences and then one more query per conference to load comments — 51 queries for one page. Making the relation lazy would not mean less work here; it would spread the same work across many small queries, which is usually slower than one larger one.
The rule of thumb:
- Eager (the default) when you usually need the related objects — for example a detail view that always renders the location and all comments.
- Lazy (
#) when you usually do not need the relation — for example a list view that shows only titles, where loading every child would be wasted work.[Lazy] - If a list view does need related data for every row, neither is ideal. It is often best to step outside the ORM and fetch what the view needs in a single query.
See also
When to drop out of the ORM — using raw DBAL from a repository when relation loading creates a performance problem.
The lazy-versus-eager decision is per relation and reversible — it is a property attribute, not a schema change — so measure with real data before optimising.
See also
- Persistence and the Extbase ORM — the ORM mental model.
- Querying the database with Extbase — storagePid, query settings, and debugging.
- Modelling relations in Extbase — declaring relations in the model.
With relations and queries understood, you have the full persistence picture of an Extbase extension. For everything the ORM does not do — aggregates, bulk writes, complex joins — see When to drop out of ORM (Object Relational Mapping).