123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- Composite and Foreign Keys as Primary Key
- =========================================
- .. versionadded:: 2.1
- Doctrine 2 supports composite primary keys natively. Composite keys are a very powerful relational database concept
- and we took good care to make sure Doctrine 2 supports as many of the composite primary key use-cases.
- For Doctrine 2.0 composite keys of primitive data-types are supported, for Doctrine 2.1 even foreign keys as
- primary keys are supported.
- This tutorial shows how the semantics of composite primary keys work and how they map to the database.
- General Considerations
- ~~~~~~~~~~~~~~~~~~~~~~
- Every entity with a composite key cannot use an id generator other than "ASSIGNED". That means
- the ID fields have to have their values set before you call ``EntityManager#persist($entity)``.
- Primitive Types only
- ~~~~~~~~~~~~~~~~~~~~
- Even in version 2.0 you can have composite keys as long as they only consist of the primitive types
- ``integer`` and ``string``. Suppose you want to create a database of cars and use the model-name
- and year of production as primary keys:
- .. configuration-block::
- .. code-block:: php
- <?php
- namespace VehicleCatalogue\Model;
- /**
- * @Entity
- */
- class Car
- {
- /** @Id @Column(type="string") */
- private $name;
- /** @Id @Column(type="integer") */
- private $year
- public function __construct($name, $year)
- {
- $this->name = $name;
- $this->year = $year;
- }
- public function getModelName()
- {
- return $this->name;
- }
- public function getYearOfProduction()
- {
- return $this->year;
- }
- }
- .. code-block:: xml
- <?xml version="1.0" encoding="UTF-8"?>
- <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
- http://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
- <entity name="VehicleCatalogue\Model\Car">
- <id field="name" type="string" />
- <id field="year" type="integer" />
- </entity>
- </doctrine-mapping>
- .. code-block:: yaml
- VehicleCatalogue\Model\Car:
- type: entity
- id:
- name:
- type: string
- year:
- type: integer
- Now you can use this entity:
- .. code-block:: php
- <?php
- namespace VehicleCatalogue\Model;
- // $em is the EntityManager
- $car = new Car("Audi A8", 2010);
- $em->persist($car);
- $em->flush();
- And for querying you can use arrays to both DQL and EntityRepositories:
- .. code-block:: php
- <?php
- namespace VehicleCatalogue\Model;
- // $em is the EntityManager
- $audi = $em->find("VehicleCatalogue\Model\Car", array("name" => "Audi A8", "year" => 2010));
- $dql = "SELECT c FROM VehicleCatalogue\Model\Car c WHERE c.id = ?1";
- $audi = $em->createQuery($dql)
- ->setParameter(1, array("name" => "Audi A8", "year" => 2010))
- ->getSingleResult();
- You can also use this entity in associations. Doctrine will then generate two foreign keys one for ``name``
- and to ``year`` to the related entities.
- .. note::
- This example shows how you can nicely solve the requirement for existing
- values before ``EntityManager#persist()``: By adding them as mandatory values for the constructor.
- Identity through foreign Entities
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- .. note::
- Identity through foreign entities is only supported with Doctrine 2.1
- There are tons of use-cases where the identity of an Entity should be determined by the entity
- of one or many parent entities.
- - Dynamic Attributes of an Entity (for example Article). Each Article has many
- attributes with primary key "article_id" and "attribute_name".
- - Address object of a Person, the primary key of the address is "user_id". This is not a case of a composite primary
- key, but the identity is derived through a foreign entity and a foreign key.
- - Join Tables with metadata can be modelled as Entity, for example connections between two articles
- with a little description and a score.
- The semantics of mapping identity through foreign entities are easy:
- - Only allowed on Many-To-One or One-To-One associations.
- - Plug an ``@Id`` annotation onto every association.
- - Set an attribute ``association-key`` with the field name of the association in XML.
- - Set a key ``associationKey:`` with the field name of the association in YAML.
- Use-Case 1: Dynamic Attributes
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- We keep up the example of an Article with arbitrary attributes, the mapping looks like this:
- .. configuration-block::
- .. code-block:: php
- <?php
- namespace Application\Model;
- use Doctrine\Common\Collections\ArrayCollection;
- /**
- * @Entity
- */
- class Article
- {
- /** @Id @Column(type="integer") @GeneratedValue */
- private $id;
- /** @Column(type="string") */
- private $title;
- /**
- * @OneToMany(targetEntity="ArticleAttribute", mappedBy="article", cascade={"ALL"}, indexBy="attribute")
- */
- private $attributes;
- public function addAttribute($name, $value)
- {
- $this->attributes[$name] = new ArticleAttribute($name, $value, $this);
- }
- }
- /**
- * @Entity
- */
- class ArticleAttribute
- {
- /** @Id @ManyToOne(targetEntity="Article", inversedBy="attributes") */
- private $article;
- /** @Id @Column(type="string") */
- private $attribute;
- /** @Column(type="string") */
- private $value;
- public function __construct($name, $value, $article)
- {
- $this->attribute = $name;
- $this->value = $value;
- $this->article = $article;
- }
- }
- .. code-block:: xml
- <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
- http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
- <entity name="Application\Model\ArticleAttribute">
- <id name="article" association-key="true" />
- <id name="attribute" type="string" />
-
- <field name="value" type="string" />
- <many-to-one field="article" target-entity="Article" inversed-by="attributes" />
- <entity>
- </doctrine-mapping>
- .. code-block:: yaml
- Application\Model\ArticleAttribute:
- type: entity
- id:
- article:
- associationKey: true
- attribute:
- type: string
- fields:
- value:
- type: string
- manyToOne:
- article:
- targetEntity: Article
- inversedBy: attributes
- Use-Case 2: Simple Derived Identity
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Sometimes you have the requirement that two objects are related by a One-To-One association
- and that the dependent class should re-use the primary key of the class it depends on.
- One good example for this is a user-address relationship:
- .. configuration-block::
- .. code-block:: php
- <?php
- /**
- * @Entity
- */
- class User
- {
- /** @Id @Column(type="integer") @GeneratedValue */
- private $id;
- }
- /**
- * @Entity
- */
- class Address
- {
- /** @Id @OneToOne(targetEntity="User") */
- private $user;
- }
- .. code-block:: yaml
- User:
- type: entity
- id:
- id:
- type: integer
- generator:
- strategy: AUTO
- Address:
- type: entity
- id:
- user:
- associationKey: true
- oneToOne:
- user:
- targetEntity: User
- Use-Case 3: Join-Table with Metadata
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- In the classic order product shop example there is the concept of the order item
- which contains references to order and product and additional data such as the amount
- of products purchased and maybe even the current price.
- .. code-block:: php
- <?php
- use Doctrine\Common\Collections\ArrayCollection;
- /** @Entity */
- class Order
- {
- /** @Id @Column(type="integer") @GeneratedValue */
- private $id;
- /** @ManyToOne(targetEntity="Customer") */
- private $customer;
- /** @OneToMany(targetEntity="OrderItem", mappedBy="order") */
- private $items;
- /** @Column(type="boolean") */
- private $payed = false;
- /** @Column(type="boolean") */
- private $shipped = false;
- /** @Column(type="datetime") */
- private $created;
- public function __construct(Customer $customer)
- {
- $this->customer = $customer;
- $this->items = new ArrayCollection();
- $this->created = new \DateTime("now");
- }
- }
- /** @Entity */
- class Product
- {
- /** @Id @Column(type="integer") @GeneratedValue */
- private $id;
- /** @Column(type="string") */
- private $name;
- /** @Column(type="decimal") */
- private $currentPrice;
- public function getCurrentPrice()
- {
- return $this->currentPrice;
- }
- }
- /** @Entity */
- class OrderItem
- {
- /** @Id @ManyToOne(targetEntity="Order") */
- private $order;
- /** @Id @ManyToOne(targetEntity="Product") */
- private $product;
- /** @Column(type="integer") */
- private $amount = 1;
- /** @Column(type="decimal") */
- private $offeredPrice;
- public function __construct(Order $order, Product $product, $amount = 1)
- {
- $this->order = $order;
- $this->product = $product;
- $this->offeredPrice = $product->getCurrentPrice();
- }
- }
- Performance Considerations
- ~~~~~~~~~~~~~~~~~~~~~~~~~~
- Using composite keys always comes with a performance hit compared to using entities with
- a simple surrogate key. This performance impact is mostly due to additional PHP code that is
- necessary to handle this kind of keys, most notably when using derived identifiers.
- On the SQL side there is not much overhead as no additional or unexpected queries have to be
- executed to manage entities with derived foreign keys.
|