Relationships
The ORM adopts the relationship types 1:1, 1:n, and n:m from the concept of relational databases. However, it is not the tables that are related to each other, but the entities.
Defining Relationships
Let's assume you want to develop a blog whose articles can be commented on. For this, the entities BlogArticle and Comment must be in a 1:n relationship with each other. An article can contain multiple comments, and a comment is always assigned to only one article. Therefore, the entity Comment should also contain a property $blogArticle of type BlogArticle.
To mark relationship properties, the attributes #[OneToOne(...)], #[ManyToOne(...)], #[OneToMany(...)], and #[ManyToMany(...)] are available.
class Comment extends ObjectAdapter {
public int $id;
public string $author;
public string $text;
#[ManyToOne(BlogArticle::class)]
public BlogArticle $blogArticle;
}
Since the property $blogArticle realizes an n:1 relationship to BlogArticle from the perspective of the entity Comment, we annotate $blogArticle with #[ManyToOne(...)].
If the relationship should be bidirectional, we need an additional property BlogArticle::$comments of type Comment[]. ArrayObject is used exclusively as the collection type for relationship properties.
class BlogArticle extends ObjectAdapter {
public int $id;
public string $title;
public string $text;
/**
* @var ArrayObject<Comment>
*/
#[OneToMany('article', Comment::class)]
public \ArrayObject $comments;
public function __construct($title = null) {
$this->title = $title;
}
}
Since the property $comments realizes a 1:n relationship to Comment from the perspective of the entity BlogArticle, we annotate $comments with #[OneToMany(...)].
In a bidirectional relationship, one property is considered the Owner. The opposite side must reference the Owner property. For this, the second parameter ($mappedBy) of #[OneToOne(...)], #[OneToMany(...)], and #[ManyToMany(...)] is intended. #[ManyToOne(...)] does not have a $mappedBy parameter and is therefore always the Owner of the relationship.
Only changes to the relationship properties on the Owner side are persisted to the database.
In the example above, the property Comment::$blogArticle is the Owner of this relationship. If you want to create a new comment and assign it to an article, you must establish the relationship via Comment::setBlogArticle().
$blogArticle = $em->find(BlogArticle::getClass(), $id);
$newComment = new Comment();
$newComment->author = 'Author';
$newComment->text = 'Lorem ipsum';
$newComment->setBlogArticle($blogArticle);
$em->persist($newComment);
Changes to BlogArticle::$comments are ignored and not persisted to the database. Therefore, the relationship is not established through the following code:
$blogArticle->comments->append($newComment); // does not affect any changes
Caution
If you forget to reference the Owner property via
$mappedByon the opposite side of a bidirectional relationship, the relationship properties on both sides are Owners. As a result, two separate relationships are created independently of each other.
The following tables would be suitable for BlogArticle and Comment:
CREATE TABLE `blog_article` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(50) NOT NULL,
`text` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `comment` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`author` varchar(32) DEFAULT NULL,
`text` text,
`blog_article_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
OneToOne (1:1) / ManyToOne (n:1)
By default, the ORM maps OneToOne and ManyToOne properties (in bidirectional relationships only the Owner property) to a join column in the table of the defining entity.
You can specify the name of the join column using n2n\persistence\orm\attributes\JoinColumn. Without this annotation, the name is generated automatically based on the property name.
class Comment extends ObjectAdapter {
#[JoinColumn('article_id')]
public int $id;
#[ManyToOne(BlogArticle::class)
public BlogArticle $blogArticle;
// rest of class body
}
The property $blogArticle is associated via the join column article_id. Without the annotation, the name of the join column would default to blog_article_id.
Alternatively, these two relationship types can also be realized via a join table. The annotation n2n\persistence\orm\annotation\AnnoJoinTable is used for this.
OneToMany (1:n) / ManyToMany (n:m)
For OneToMany and ManyToMany properties (in bidirectional relationships only on the Owner side), the ORM expects a join table by default.
The names of the join table and the join columns can be specified using n2n\persistence\orm\annotation\AnnoJoinTable. Without the annotation, these are generated automatically.
Additionally, for both relationship types — regardless of whether it is the Owner side — the sorting of the entities within the collection can be defined using n2n\persistence\orm\annotation\AnnoOrderBy.
class BlogArticle extends ObjectAdapter {
/**
* @var ArrayObject<Category>
*/
#[ManyToMany(Category::class)
#[OrderBy(array('name' => 'ASC')))]
public \ArrayObject $categories;
public function setCategories(\ArrayObject $categories) {
$this->categories = $categories;
}
// rest of class body
}
In this example, we realize an n:m relationship between the entity BlogArticle and the entity Category.
For this, we define the ManyToMany property BlogArticle::$categories. When loading from the database, the ORM automatically populates this property with an ArrayObject containing Category entities. The collection is sorted by Category::$name in ascending order (ASC).
OneToMany (1:n) / ManyToMany (n:m)
The relationship should be bidirectional, which is why we implement a property $blogArticles for the entity Category. The "Owner" is BlogArticle::$categories.
#[ManyToMany(BlogArticle::class, 'categories')]
class Category extends ObjectAdapter {
public int $id;
public string $name;
public \ArrayObject $blogArticles;
public function __construct($name = null) {
$this->name = $name;
}
}
The following table is required for the example above (note especially the join table blog_article_categories):
CREATE TABLE `blog_article` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(50) NOT NULL,
`text` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `blog_article_categories` (
`blog_article_id` int(10) unsigned NOT NULL,
`category_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`blog_article_id`, `category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `category` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
As described above, you can also determine the names of the join table and its join columns yourself. To do this, you annotate the "Owner" property with AnnoJoinTable.
#[ManyToMany(Category::class)]
#[JoinTable('junction_table',
'join_column',
'inverse_join_column'))]
#[OrderBy(array('name' => 'ASC'))]
OneToMany Join Column
For OneToMany properties (in bidirectional relationships only for the "Owner"), you can also annotate a join column via JoinColumn. This is looked up in the table of the opposite side. In this way, the 1:n relationship between BlogArticle and Comment can also be realized.
use Dom\Comment;
class BlogArticle extends ObjectAdapter {
/**
* @var ArrayObject<Comment>
*/
#[OneToMany(Comment::class)]
#[JoinColumn]
public \ArrayObject $comments;
// rest of class body
}
In contrast to the example above, the property BlogArticle::$comments is now the "Owner".
class Comment extends ObjectAdapter {
#[ManyToOne(targetEntity: BlogArticle::class)]
public BlogArticle $blogArticle;
// rest of class body
}
The tables remain identical:
CREATE TABLE `blog_article` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(50) NOT NULL,
`text` TEXT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `comment` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`author` VARCHAR(32) DEFAULT NULL,
`text` TEXT,
`blog_article_id` INT(10) UNSIGNED DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Cascade
Via the $cascadeType parameter of the attributes #[OneToOne(...)], #[OneToMany(...)], #[ManyToOne(...)], and #[ManyToMany(...)], you can define which operations should be propagated to the annotated property. The following types are available:
n2n\persistence\orm\CascadeType::PERSISTn2n\persistence\orm\CascadeType::MERGEn2n\persistence\orm\CascadeType::REMOVEn2n\persistence\orm\CascadeType::REFRESHn2n\persistence\orm\CascadeType::DETACHn2n\persistence\orm\CascadeType::ALL
For example, if you define CacscadeType::PERSIST for the property BlogArticle::$comments and persist an entity BlogArticle with EntityManager::persist(), all entities Comment that are assigned to the property BlogArticle::$comments at that point in time are also persisted.
class BlogArticle extends EntityAdapter {
/**
* @var ArrayObject<Comment>
*/
#[OneToMany(Comment::class, 'blogArticle', cascade: CascadeType::PERSIST)]
public \ArrayObject $comments;
// rest of class body
}
As described in the article Lifecylce, during a Flush event a persist operation is applied to every entity (with Status MANAGED). This now means that we can add a new comment to an article that is automatically persisted on commit (a commit leads to a Flush event).
public function doNewComment(BlogArticleDao $blogArticleDao, $blogArticleId) {
$this->beginTransaction();
$blogArticle = $blogArticleDao->getBlogArticleById($blogArticleId);
if ($blogArticle === null) {
throw new PageNotFoundException();
}
$newComment = new Comment();
$newComment->author = 'Author';
$newComment->text = 'Lorem ipsum';
$newComment->setBlogArticle($blogArticle);
$blogArticle->comments->append($newComment);
$this->commit();
}
Since we annotated CascadeType::PERSIST for BlogArticle::$comments, the new entity Comment is automatically persisted on commit. However, since the property BlogArticle::$comments is not the "Owner" of the relationship, we still have to assign the corresponding entity BlogArticle to the new entity Comment via Comment::setBlogArticle() so that the relationship is established.
You can also combine cascade types:
#[OneToMany(Comment::class, 'blogArticle', cascade: CascadeType::PERSIST|CascadeType::REMOVE)]
class BlogArticle extends EntityAdapter {
// rest of class body
}
If you were to delete an article now (EntityManager::remove()), all of its comments would also be deleted.
If all operations should be propagated to a property, you can also use CascadeType::ALL.
#[ManyToOne(targetEntity: Category::class, cascade: null, cascadeType: CascadeType::ALL)]
Fetch Type
Via the $fetchType parameter of the attributes #[OneToOne(...)], #[OneToMany(...)], #[ManyToOne(...), and #[ManyToMany(...)], you determine when the entity / the entities of a property should be loaded from the database. The options are n2n\persistence\orm\FetchType::LAZY and n2n\persistence\orm\FetchType::EAGER. The default for all relationship types is FetchType::LAZY.
class Comment extends EntityAdapter {
#[ManyToOne(BlogArticle::class)
public BlogArticle $blogArticle;
// rest of class body
}
In this example, FetchType::LAZY applies to the property Comment::$blogArticle. This has the consequence that the ORM only loads the properties of BlogArticle the first time it is accessed (a SELECT statement is executed).
$comment = $em->find(Comment::getClass(), $commentId);
$blogArticle = $comment->getBlogArticle();
var_dump($blogArticle->getTitle()); // BlogArticle properties get loaded here
In this example, the properties of BlogArticle are only loaded when $blogArticle->getTitle() is called.
For OneToOne and ManyToOne properties,
FetchType::LAZYis only possible for entities that do not have any properties withpublicvisibility.FetchType::LAZYis also not usable for entities that use inheritance and have sub-entities.
For OneToMany or ManyToMany properties with FetchType::LAZY, the entities are loaded as soon as fields of the array (ArrayObject) are accessed for the first time (for example through a foreach loop).
$blogArticle = $em->find(BlogArticle::getClass(), $id);
$categories = $blogArticle->categories;
foreach ($categories as $category) { // categories get loaded here
test($category->getName());
}
The entities of all fields are always loaded, even if you only access a single field (e.g. $comments[0]).
If you choose FetchType::EAGER, the entity or the entities of this property are loaded immediately after loading the defining entity (an additional SELECT statement is executed).
class Comment extends EntityAdapter {
#[ManyToOne(targetEntity: BlogArticle::class, cascade: null, fetch: FetchType::EAGER)]
public BlogArticle $blogArticle;
// rest of class body
}
Since we chose FetchType::EAGER for the property Comment::$blogArticle in this example, the properties of BlogArticle are loaded immediately after the query of Comment.
$comment = $em->find(Comment::getClass(), $commentId); // BlogArticle properties get loaded here
$blogArticle = $comment->getBlogArticle();
var_dump($blogArticle->getTitle());
FetchType::EAGER can be useful when working with transactions.
Fetch Join
With a fetch join, you force properties to be loaded immediately during the query (SELECT statement is extended by a JOIN), and not later with an additional SELECT statement. This is also possible if FetchType::LAZY applies to them. You activate a fetch join via the 4th parameter of the method Criteria::joinProperty().
$criteria = $em->createCriteria();
$criteria->select('c')
->from(Comment::getClass(), 'c')
->joinProperty('c.blogArticle', 'ba', null, true)
->where()->match('c.id', '=', $commentId);
Through the fetch join, the loading of Comment::$blogArticle is integrated into the query in this example.
In NQL, you achieve this behavior via the keyword JOIN FETCH.
$criteria = $em->createNqlCriteria(
'SELECT c FROM Comment c JOIN FETCH c.blogArticle WHERE c.id = :commentId',
array('commentId' => $commentId));
Orphan Removal
Via the last parameter of #[OneToOne(...)] and #[OneToMany(...)], you enable "Orphan Removal". If the relationship to entities that were referenced by this property is removed, they are deleted. This happens, for example, when you set a OneToOne property to null or remove entities from the ArrayObject of a OneToMany property.
#[OneToMany('blogArticle', Comment::class)]
class BlogArticle extends EntityAdapter {
/**
* @var \ArrayObject<Comment>
*/
publie \ArrayObject $comments;
/**
* @return Comment[]
*/
// rest of class body
}
If you now remove a comment from an article, this comment is deleted.
public function doRemoveAllComments(BlogArticleDao $blogArticleDao, $blogArticleId) {
$this->beginTransaction();
$blogArticle = $blogArticleDao->getBlogArticleById($blogArticleId);
if ($blogArticle === null) {
throw new PageNotFoundException();
}
$blogArticle->getComments()->exchangeArray(array());
$this->commit();
}
In this example, all comments of an article are deleted.
Relationships in Queries
Relationship properties can be perfectly integrated into queries. With the "." operator, you can, for example, realize "joins" across properties.
Example 1 (WHERE)
In this example, we query all comments that are assigned to an article with the title $title.
Criteria API
$criteria = $em->createSimpleCriteria(Comment::getClass(), array('blogArticle.title' => $title));
NQL
$criteria = $em->createNqlCriteria(
'SELECT c FROM Comment c WHERE c.blogArticle.title = :title',
array('title' => $title));
Example 2 (WHERE)
For comparisons with ToOne properties, you can use the comparison operators =, !=, IN, NOT IN. The variable $blogArticles in the following example contains an array of Comment entities.
Criteria API
$criteria = $em->createCriteria();
$criteria->select('c')
->from(Comment::getClass(), 'c')
->where()->match('c.blogArticle', 'IN', $blogArticles);
NQL
$criteria = $em->createNqlCriteria(
'SELECT c FROM Comment c WHERE c.blogArticles IN (:blogArticles)',
array('blogArticles' => $blogArticles));
Example 3 (WHERE)
For comparisons with ToMany properties, you can use the comparison operators CONTAINS and CONTAINS NOT. The variable $category in the following example contains an entity Category.
Criteria API
$criteria = $em->createCriteria();
$criteria->select('a')
->from(BlogArticle::getClass(), 'a')
->where()->match('a.categories', 'CONTAINS', $category);
NQL
$criteria = $em->createNqlCriteria(
'SELECT a FROM BlogArticle a WHERE a.categories CONTAINS :category',
array('category' => $category));
Example 4 (SUB SELECT)
Criteria API
$deniedTitle = 'Test';
$criteria = $em->createCriteria();
$criteria->select('c')
->from(Comment::getClass(), 'c')
->where()->match('c.blogArticle', 'NOT IN', $criteria->subCriteria()
->select('ba')
->from(BlogArticle::getClass(), 'ba')
->where()->match('ba.title', '=', $deniedTitle)->endWhere());
NQL
$deniedTitle = 'Test';
$criteria = $em->createNqlCriteria(
'SELECT c FROM Comment c WHERE c.blogArticle NOT IN (
SELECT ba FROM BlogArticle ba WHERE ba.title = :deniedTitle)',
array('deniedTitle' => $deniedTitle));
I am converting the contents to Markdown. The previous HTML blocks were very long; I output the conversion section by section so that nothing gets cut off.
Relationships in Embedded Extra Classes and Mapped Superclasses
Via n2n\persistence\orm\attribute\AttributeOverrides (article Attribute Overrides) it is not possible to override the names of join columns and join tables.
For this, the annotation n2n\persistence\orm\annotation\AnnoAssociationOverrides exists.
It works the same way as AttributeOverrides, but expects:
- As first parameter an
arrayofAnnoJoinColumnannotations. - As second parameter an
arrayofAnnoJoinTableannotations.
Example
class Member extends ObjectAdapter {
public int $id;
#[Embedded(Assignement::class)]
#[AssociationOverrides(array( 'assignementGroup' => new JoinColumn('group_id')))]
private $assignement;
// rest of class body
}
class Assignement extends ObjectAdapter {
public string $name;
#[ManyToOne(AssignementGroup::class)]
private $assignementGroup;
// rest of class body
}
A relationship property can only realize relationships to entities (not to extra classes), even if the Owner property is located in an extra class — as in this example.
Note the $mappedBy parameter of #[OneToMany(..)] in the following code excerpt.
class AssignementGroup extends ObjectAdapter {
public int $id;
public string $name;
#[OneToMany('assignement.assignementGroup', Member::class)]
public string $members;
// rest of class body
}
The following tables match the example above:
CREATE TABLE `member` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`group_id` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `assignement_group` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;