In a one–to–many relationship, one object holds one or more references to a another object, like, for example, the relationship between an author and the book(s) he or she has written:
<?php
class Author {
$books = array();
}
A Book, is associated with only one Author.
<?php
class Book {
$author
}
Author and Book have a one–to–many/many–to–one bidirectional realtionship, in which Book is the many–to–one side of the relationship. A author “has written” one or more (one to many) books, but each indvidual Book “has been written by” just one author.
Note
In the real world, a book, of course, may have more than one author, but we are not modeling that here.
In general, a one–to–many/many–to–one relationship exists when one end of the relationship contains a collection, but the other end does not. Author contains a collection of Book objects, but each Book has only one Author. The Author and Book relationship is considered bi-directional because Book holds a reference back its Author.
As an example of a many–to–many relationship, consider the relationship between meetup.com groups and members of the meetup.com website. A MeetupMember “belongs to” or “has joined” zero to many MeetupGroup(s). A MeetupGroup has zero to many Meetup members.
<?php
class MeetupGroup {
$members = array();
}
<?php
class MeetupMember {
$meetupgroups = array();
}
Each end of the relationship is a collection. That is how we know that it is a many–to–many relationship.
In this model, a Car “contains an engine” and that particular Engine “is contained in” only that one Car.
<?php
class Car {
$model;
$engine;
}
<?php
class Engine {
$car;
}
Neither end of the relationship is a collection. The relationship is bi-directional: the object on either end of the relationship knows about the other object.
What is the difference between one–to–one and many–to–one relationships? In many–to–one relationships (which are synonymous with one–to–many relationships), one side of the association can reference several objects at the other end of the relationship. In a one–to–one relationship, the multiplicy is never greater than one on either end of the relationship.
One to Many relationships occur when each record in the first table has many linked records in the related table, but each record in the related table has only one corresponding record in the first table. In the Author and Book example, our database would look something like this
| author_id | name |
|---|---|
| 1 | Joe Smith |
| 2 | Bob Ames |
| book_id | title | publication_date | author_id | ||||
|---|---|---|---|---|---|
| 1 | Latin Made Easy | 09-09-1985 | 1 | ||
| 1 | Estonian Made Easy | 09-09-1985 | 2 | ||
We could also implement this with three tables
| author_id | name |
|---|---|
| 1 | Joe Smith |
| 2 | Bob Ames |
| book_id | title | publication_date |
|---|---|---|
| 1 | Latin Made Easy | 09-09-1985 |
| 1 | Estonian Made Easy | 09-09-1985 |
| book_id | author_id |
|---|---|
| 1 | 1 |
| 1 | 2 |
But then book_id would need to be made UNIQUE.
Many to Many relationships occur when each record in the first table has many linked records in the related table and vice-versa. In a normalized database, these are always stored using dedicated join tables.
| member_id | name |
|---|---|
| 1 | Bill Jones |
| 2 | Bob Ames |
| group_id | name |
|---|---|
| 1 | Latin Club |
| 2 | Bridge |
| member_id | group_id |
|---|---|
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 2 | 2 |
The many–to–many relationship actually consists of two one–to–many relationships, both of which involve the association or join table.
In a one to one relationship, one table holds a foreign key to the other table.
| car_id | model |
|---|---|
| 1 | Mustang |
| 2 | BMW Series 1 |
| engine_id | type | car_id |
|---|---|---|
| 1 | V8 | 2 |
| 2 | Hybrid | 1 |
This could also be implemented with Car holding the foreign key, but we would then need to specify engine_id as unique.
In object bi–directional relationship, like all those above, the object on either end of the relationship knows about the object at the opposite end. In a uni–directional object relationship only one end of the relationship knows about the other end. The other end does not have a reference that refers back to the opposite end. Object relationships can be unidirectional, like this example:
<?php
class Position {
protected $title;
}
class Employee {
protected $position;
public setPosition(Position $p)
{
$this->position = $p;
}
// snip...
}
Here Employee objects know about the position that they hold, but Position objects do not know which employee holds it (this example is from Scott Amlber’s Mapping Objects to Relational Databases).
As another example of a uni–directional
In UML such uni–directional relationships are depicted with an arrow, and bi–directional relationships are depicted with a straight line:
Since some positions can go unfilled, the relationship from Employee to Postion is 0..1 (rather than 1). As another example, consider the many–to–one uni–directional relationship between Stamp and its Country of issuance.
A stamp is “issued by” just one country, but that country “issues” many stamps.
Because foreign keys are used to join tables, all relationships in a relational database are effectively bi-directional.
This example is available on Github at https://gist.github.com/1307410. It is an example of a bi–directional many–to–one relationship between Users and UserGroup. A UserGroups “has” a zero to many User. A User is a member of one UserGroup, although the User may not be a member of any UserGroup.
<?php
namespace HelloWorld;
use InvalidArgumentException;
/**
* This class is somewhere in your library
* @Entity
* @ORM\Table(name="users")
*/
class User {
/**
* @var int
* @Id
* @ORM\Column(type="integer",name="id", nullable=false)
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
* @ORM\Column(type="string", length=255, name="login", nullable=false)
*/
protected $login;
/**
* @var UserGroup|null the group this user belongs (if any)
* @ORM\ManyToOne(targetEntity="HelloWorld\UserGroup", inversedBy="users")
* @ORM\JoinColumn(name="group_id", referencedColumnName="id")
*/
protected $group;
/**
* @param string $login
*/
public function __construct($login)
{
$this->setLogin($login);
}
/**
* @return string
*/
public function getLogin()
{
return $this->login;
}
/**
* @param string $login
*/
public function setLogin($login)
{
$this->login = (string) $login;
}
/**
* @return HelloWorld\UserGroup|null
*/
public function getGroup()
{
return $this->group;
}
/**
* Sets a new user group and cleans the previous one if set
* @param null|HelloWorld\UserGroup $group
*/
public function setGroup($group)
{
if($group === null) {
if($this->group !== null) {
$this->group->getUsers()->removeElement($this);
}
$this->group = null;
} else {
if(!$group instanceof HelloWorld\UserGroup) {
throw new InvalidArgumentException('$group must be null or instance of HelloWorld\UserGroup');
}
if($this->group !== null) {
$this->group->getUsers()->removeElement($this);
}
$this->group = $group;
$group->getUsers()->add($this);
}
}
}
<?php
namespace HelloWorld;
use Doctrine\Common\Collections\Collection,
Doctrine\Common\Collections\ArrayCollection;
/**
* This class is somewhere in your library
* @Entity
* @ORM\Table(name="usergroups")
*/
class UserGroup {
/**
* @var int
* @Id
* @ORM\Column(type="integer",name="id", nullable=false)
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
* @ORM\Column(type="string", length=255, name="name", nullable=false)
*/
protected $name;
/**
* @var Collection
* @ORM\OneToMany(targetEntity="HelloWorld\User", mappedBy="group")
*/
protected $users;
/**
* @param string $name
*/
public function __construct($name)
{
$this->users = new ArrayCollection();
$this->setName($name);
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @var string $name
*/
public function setName($name)
{
$this->name = (string) $name;
}
/**
* @return Collection
*/
public function getUsers()
{
return $this->users;
}
}
Below is the database schema diagram for the ZendDb class example that is the basis for the Cookbook example.
The diagram shows the primary keys PK and the foreign keys FK. The database is used to track bugs which have been “reported by” users. Once reported, bugs are “assigned to” certain users (who are engineers) to be fixed. Bugs can occurs in one or more products. In the real world, bugs most likely would be reported against only one product, but in this example the same bug can be reported in more than one product.
The purely object–based model used in the Cookbook example looks like this:
We have two one–to–many/many–to–one relationships between class User and class Bug: 1.) the bugs “reported by” a user and 2.) the bugs “assigned to” that user. Both of these relationships are bi–directional: the User knows both the Bug(s) he or she reported and the Bug(s) he or she is assigned to fix; similarly, the Bug knows both who reported it and who is assigned to fix it.
We also have a single many–to–many relationship between bugs and the products they have been “reported in”. This relationship is uni–directional: the Bug knows about the Product(s) in which it occurs, but the Product is completely unaware of these Bugs.
Classes to be persisted by Doctrine are referred to as entities. So Bug, User and Product are entities. You cannot use cannot use php arrays for relationships you want to persist. Instead must use Doctrine\Common\Collection\ArrayCollection in your entities. And entities cannot have public properties.
In order to persist these relationships, one end of the relationship needs to be designated the “owning side”. Changes to the owning side determine whether the relationship is updated in the database. In all bi–directional one-to-many/many-to-one, the owning side is the many-to-one side. The opposite side, the inverse side, contains the ArrayCollection (one-to-many unidirectional relationships are discussed later).
<?php
/**
* @ORM\Entity @Table(name="bugs")
*/
class Bug {
/**
* @ORM\Id @Column(type="integer") @GeneratedValue
*/
public $id;
/**
* @ORM\Column(type="string")
*/
public $description;
/**
* @ORM\Column(type="datetime")
*/
public $created;
/**
* @ORM\Column(type="string")
*/
public $status;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="assignedBugs")
*/
private $engineer;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="reportedBugs")
*/
private $reporter;
/**
* @ORM\ManyToMany(targetEntity="Product")
*/
private $products;
}
Important
Owning side and inverse side are ORM concepts, not concepts of your domain model. The important thing to remember is: your code must insure both ends of the relationship are properly maintained.
We implement Bug::setEngineer(User $u) and Bug::setReporter(User $u) in a way that ensures the consistency of the references back and forth to the assigned and reported bugs from a user:
<?php
class MyProject\Entity\User;
class Bug {
// member variables shown above
public function setEngineer(User $engineer)
{
$engineer->assignedToBug($this);
$this->engineer = $engineer;
}
public function setReporter(User $reporter)
{
$reporter->addReportedBug($this);
$this->reporter = $reporter;
}
}
Whenever a new bug is saved or an engineer is assigned to the bug, we don’t want to update the User to persist the reference, but the Bug.
Class User looks like this. User is the inverse side of the “reported by” and “assigned to” bi–directional relationships between Bug and User.
<?php
use MyProject\Entity\Bug;
/**
* @ORM\Entity @Table(name="users")
*/
class User {
/**
* @ORM\Id @GeneratedValue @ORM\Column(type="integer")
*/
public $id;
/**
* @ORM\Column(type="string")
*/
public $name;
/**
* @ORM\OneToMany(targetEntity="Bug", mappedBy="reporter")
*/
protected $reportedBugs = null;
/**
* @ORM\OneToMany(targetEntity="Bug", mappedBy="engineer")
*/
protected $assignedBugs = null;
public function addReportedBug($bug)
{
$this->reportedBugs[] = $bug;
}
public function assignedToBug($bug)
{
$this->assignedBugs[] = $bug;
}
Because Bugs reference Products by a uni-directional many–to–many relation in the database that points from from Bugs to Products, the mapping’s for class Product are quite easy:
<?php
/**
* @ORM\Entity @Table(name="products")
*/
class Product {
/** @ORM\Id @Column(type="integer") @ORM\GeneratedValue */
private $id;
/** @ORM\Column(type="string") */
private $name;
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
Client code (greatly simplified) would look something like:
<?php
use MyProject\Entity\User,
MyProject\Entity\Bug,
MyProject\Entity\Product;
$reporter = new User("Bob Smith");
$product = new Product("OpenOffice Writer);
$bug = new Bug();
$bug->assignToProduct($product);
$bug->setReporter($reporter);
$engineer = new User("Barbara Holz");
$bug->setEngineer($engineer);
For production code, you will need to generate your entities using the doctrine command line tool
# doctrine orm:schema-tool:create --dump-sql > cookbook.sql
Doctrine will automatically generate SQL for creating the bug_product join table. However, if you decide to change the database column names for the $id property, then you should use the @JoinColumn and @JoinTable in your relationship mappings. Here is what it would look like. First, class Bug:
<?php
namespace Entities;
use Doctrine\Common\Collections\ArrayCollection;
/** @ORM\Entity @Table(name="bugs") */
class Bug {
/** @ORM\Id @Column(type="integer", name="bug_id") @ORM\GeneratedValue */
protected $id;
// snip... $description, $status, $create omitted for brevity
/**
@ORM\ManyToOne(targetEntity="User", inversedBy="reportedBugs")
@ORM\JoinColumn(name="reporter_id", referencedColumnName="account_id")
*/
protected $reporter;
/**
@ORM\ManyToOne(targetEntity="User", inversedBy="assignedBugs")
@ORM\JoinColumn(name="engineer_id", referencedColumnName="account_id")
*/
protected $engineer;
/**
@ORM\ManyToMany(targetEntity="Product")
@ORM\JoinTable(name="bugs_products",
schema="bugs_products",
joinColumns={@JoinColumn(name="bug_id", referencedColumnName="bug_id")},
inverseJoinColumns={@JoinColumn(name="product_id", referencedColumnName="product_id")}
)
*/
protected $products;
public function __construct(User $engineer=null,
User $reporter=null,
\DateTime $datetime=null,
$status=null,
$description=null) { // snip... }
public function assignToProduct(Product $product)
{
$this->products[] = $product;
}
public function setEngineer(User $engineer)
{
$engineer->assignedToBug($this);
$this->engineer = $engineer;
}
public function setReporter(User $reporter)
{
$reporter->addReportedBug($this);
$this->reporter = $reporter;
}
public function getEngineer()
{
return $this->engineer;
}
public function getReporter()
{
return $this->reporter;
}
public function getProducts()
{
return $this->products;
}
}
Class User
<?php
namespace Entities;
use Doctrine\Common\Collections\ArrayCollection;
/** @ORM\Entity @Table(name="accounts") */
class User {
/** @ORM\Id @Column(type="integer", name="account_id") @ORM\GeneratedValue */
protected $id;
/** @ORM\Column(type="string", name="name") */
protected $name;
/** @ORM\OneToMany(targetEntity="Bug", mappedBy="reporter") */
protected $reportedBugs;
/** @ORM\OneToMany(targetEntity="Bug", mappedBy="engineer") */
protected $assignedBugs;
public function __construct($name=null)
{
$this->name = $name;
$this->reportedBugs = new ArrayCollection();
$this->assignedBugs = new ArrayCollection();
$this->products = new ArrayCollection();
}
public function addReportedBug(Bug $bug)
{
// Question is the ArrayCollection implicitly index by the id of the element?
$this->reportedBugs[] = $bug;
}
public function assignedToBug(Bug $bug)
{
$this->assignedBugs[] = $bug;
}
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
?>
Class Product:
<?php
namespace Entities;
/** @ORM\Entity @Table(name="products") */
class Product {
/** @ORM\Id @Column(type="integer", name="product_id") @ORM\GeneratedValue */
protected $id;
/** @ORM\Column(name="name", type="string", length="100") */
protected $name;
// snip ..
}
?>
A unidirectional one–to–many association must be mapped through a join table. It is mapped as a unidirectional many–to–many relation with a uniqueness constraint on the many-side of the join–table that enforces the one–to–many cardinality. The following example sets up such a unidirectional one–to–many association:
<?php
/** @ORM\Entity */
class User {
// ...
/** One-to-many, unidirectional
*
* @ORM\ManyToMany(targetEntity="Phonenumber")
* @ORM\JoinTable(name="users_phonenumbers",
* joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)}
* )
*/
private $phonenumbers;
public function __construct()
{
$this->phonenumbers = new \Doctrine\Common\Collections\ArrayCollection();
}
// ...
}
/** @ORM\Entity */
class Phonenumber {
// ...
}
Important
Again, One-To-Many uni-directional relations must be mapped as a uni–directional many–to–many association using a join–table with a unique=true constraint added onto the many side of the join table.
See also One–To–Many, Unidirectional with Join Table example.
The following list summarizes the key points about mapping object relationships:
For a good article on mapping objectes to relational databases, see Scott Ambler’s Mapping Objects to Relational Databases: O/R Mapping In Detail.