Practical PHP Patterns: Foreign Key Mapping
Join the DZone community and get the full member experience.
Join For FreeThe Foreign Key Mapping pattern is strictly linked to the concept of relationship between entities in a model, that in object models is represented as references between different objects kept in their internal fields (which may be public in certain cases).
There are two different models of relationships that are mapped onto each other by this pattern:
- the relational model stores a relationship as an equality in the value of two specific fields on different rows. As you certainly know, this fields are called keys and may be primary (unique throughout their table) or foreign (references to an unique primary key on another row, possibly of a different table).
- the object model sees a relationship as a pointer, or as an handler variable in php. The underlying concept of handlers is the same of pointers: unambiguous references to an object kept in the memory. I prefer to use the pointer term because it is more diffused and renders well the image of a link to another object.
Object relationships, when navigable in more than a single direction, are represented also by a back pointer, which must be kept in sync with the main pointer. The relational model is implicitly bidirectional instead, since you can make queries which found both sides of the relationship in every different kind of foreign key mapping.
Mapping the different models
Relationships are what makes Orms hard to code: translating between the two different models is an hard task which is supported by lots of metadata provided by the end user. Lazy loading is fundamental here as it breaks the object graph by inserting proxies at the boundaries of the loaded part, and adds complexity to the logic of the Orm.
The simplest definition of the Foreign Key Mapping pattern is that foreign keys transform in relationships in the object model, so instead of finding a id_EntityName field on an object, you will find the real object represented by this foreign key (or at least a proxy that stands for it). In this sense, the object model is an higher level description than the relational one, although this enhancement results often in complication in the maintenance of the relationships. However, you ideally don't need to perform queries as you can traverse the whole object graph trough relationships, of course causing many lazy loading queries to be executed.
Older PHP Orms, like Doctrine 1 itself, kept the foreign keys on the objects, but this solution is a duplication which can lead to inconsistencies like an object pointed by the relationship different from the one specified by the foreign key. This inconsistencies are dealt with magic behavior that synchronizes the two representations upon editing, but it is more an hack than a standard solution.
In fact, Doctrine 2 only consider object references (on their owning side) as the source of relationship data. These references must be maintained by the application developer when part of the object graph change. Foreign keys are only saved in hidden fields in proxies to simplify lazy loading, but are not employed during the commitment of the Unit of Work.
Other patterns
The Identity Field pattern is propedeutical to Foreign Key Mapping, because the foreign keys added to the various tables always refer to an Identity Field equivalent to the primary key. From the set of pointers between objects you can get the relationships targets and their Identity Fields, so that you can rebuild the foreign keys for the original object. Actually, the set of pointers (annotated with mapping metadata) is by definition what the Orm has to store together with the annotated private fields.
Relationships between entitites
There are different kinds of relationships between entities, named by the number of objects involved in the two sides of them:
- one to one relationship. An additional foreign key field is put on one of the rows that represent the objects, but it is not present on the objects themselves. This is the simplest case of relationship.
- One to many or many to one relationship: an additional foreign key field is added on the many side, so that it points to the one side (since foreign keys can only point to a single value in any relational database). This is rather awkward when the relationship has only one direction going from the one to the many side instead, to the point that some implementations use an additional table like the relationship were a many-to-many one.
- Many to many relationship: an additional table is created that contains a row for every connection between a source and a target, and both fields of this associating table are foreign keys. This table originally had a corrispective in the object model to simplify the mapping in Active Record implemetations (the in-famous UserGroup class often found in Doctrine 1 code samples). With a Data Mapper, it is not needed anymore as a domain object since its data are function of the object graph, and are written incrementally (by calculating changes) to the database, basing on the graph diff that the Unit of Work produces.
A plea for simplification
Orms are not infallible, and not all the use cases are usually supported. You should help your Orm not only with metadata but also by simplifying the model as much as possible, asking yourself some questions on your modelization. Consider the following traits of relationships:
- directionality: is the relationship navigable in both directions, and does it make sense? Eliminating one direction simplifies the maintenance and the possibility of triggering bugs in mapping or in client code.
- deducibility: is the relationship obtainable by other objects? If so, you can expose it to client code by calculation (instead of storing it).
- keys involved: primary keys are much simpler to transport when they are composed of a single field. Compound keys are a common source of bugs and strange behavior or performance.
- existence itself: is the relationship needed for business logic or it only sounds useful? You aren't gonna need it.
The fewer relationships are defined in a model, the simple it becomes to map to different kind of databases, and in general to maintain.
Code sample
This example is taken from the Doctrine 2 test suite, and it shows the mandatory metadata necessary to map an object relationship to a foreign key, here provided as annotations on the entity classes.
Note that Doctrine 2 infers the name of the columns by convention if not specified, especially for foreign keys. The examples in the test suite are very helpful for starting out with the Orm, anyway.
<?php
namespace Doctrine\Tests\Models\CMS;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Entity
* @Table(name="cms_users")
*/
class CmsUser
{
/**
* @Id @Column(type="integer")
* @GeneratedValue
*/
public $id;
/**
* @Column(type="string", length=50)
*/
public $status;
/**
* @Column(type="string", length=255, unique=true)
*/
public $username;
/**
* @Column(type="string", length=255)
*/
public $name;
/**
* @OneToMany(targetEntity="CmsPhonenumber", mappedBy="user", cascade={"persist", "remove", "merge"}, orphanRemoval=true)
*/
public $phonenumbers;
/**
* @OneToMany(targetEntity="CmsArticle", mappedBy="user")
*/
public $articles;
/**
* @OneToOne(targetEntity="CmsAddress", mappedBy="user", cascade={"persist"})
*/
public $address;
/**
* @ManyToMany(targetEntity="CmsGroup", inversedBy="users", cascade={"persist"})
* @JoinTable(name="cms_users_groups",
* joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
public $groups;
public function __construct() {
$this->phonenumbers = new ArrayCollection;
$this->articles = new ArrayCollection;
$this->groups = new ArrayCollection;
}
public function getId() {
return $this->id;
}
public function getStatus() {
return $this->status;
}
public function getUsername() {
return $this->username;
}
public function getName() {
return $this->name;
}
/**
* Adds a phonenumber to the user.
*
* @param CmsPhonenumber $phone
*/
public function addPhonenumber(CmsPhonenumber $phone) {
$this->phonenumbers[] = $phone;
$phone->setUser($this);
}
public function getPhonenumbers() {
return $this->phonenumbers;
}
public function addArticle(CmsArticle $article) {
$this->articles[] = $article;
$article->setAuthor($this);
}
public function addGroup(CmsGroup $group) {
$this->groups[] = $group;
$group->addUser($this);
}
public function getGroups() {
return $this->groups;
}
public function removePhonenumber($index) {
if (isset($this->phonenumbers[$index])) {
$ph = $this->phonenumbers[$index];
unset($this->phonenumbers[$index]);
$ph->user = null;
return true;
}
return false;
}
public function getAddress() { return $this->address; }
public function setAddress(CmsAddress $address) {
if ($this->address !== $address) {
$this->address = $address;
$address->setUser($this);
}
}
}
Opinions expressed by DZone contributors are their own.
Comments