2011-02-05 15:43:45 +03:00
|
|
|
Working with Indexed Assocations
|
|
|
|
================================
|
|
|
|
|
2011-02-05 18:14:51 +03:00
|
|
|
.. note::
|
2011-02-05 15:43:45 +03:00
|
|
|
|
|
|
|
This feature is scheduled for version 2.1 of Doctrine and not included in the 2.0.x series.
|
|
|
|
|
|
|
|
Doctrine 2 collections are modelled after PHPs native arrays. PHP arrays are an ordered hashmap, but in
|
|
|
|
the first version of Doctrine keys retrieved from the database were always numerical unless ``INDEX BY``
|
|
|
|
was used. Starting with Doctrine 2.1 you can index your collections by a value in the related entity.
|
|
|
|
This is a first step towards full ordered hashmap support through the Doctrine ORM.
|
|
|
|
The feature works like an implicit ``INDEX BY`` for the selected association but has several
|
|
|
|
downsides also:
|
|
|
|
|
|
|
|
- You have to manage both the key and field if you want to change the index by field value.
|
|
|
|
- On each request the keys are regenerated from the field value not from the previous collection key.
|
|
|
|
- Values of the Index-By keys are never considered during persistence, it only exists for accessing purposes.
|
|
|
|
- Fields that are used for the index by feature **HAVE** to be unique in the database. The behavior for multiple entities
|
|
|
|
with the same index-by field value is undefined.
|
|
|
|
|
|
|
|
As an example we will design a simple stock exchange list view. The domain consists of the entity ``Stock``
|
|
|
|
and ``Market`` where each Stock has a symbol and is traded on a single market. Instead of having a numerical
|
|
|
|
list of stocks traded on a market they will be indexed by their symbol, which is unique across all markets.
|
|
|
|
|
|
|
|
Mapping Indexed Assocations
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
You can map indexed assocations by adding:
|
|
|
|
|
|
|
|
* ``indexBy`` attribute to any ``@OneToMany`` or ``@ManyToMany`` annotation.
|
|
|
|
* ``index-by`` attribute to any ``<one-to-many />`` or ``<many-to-many />`` xml element.
|
|
|
|
* ``indexBy:`` key-value pair to any association defined in ``manyToMany:`` or ``oneToMany:`` YAML mapping files.
|
|
|
|
|
|
|
|
The code and mappings for the Market entity looks like this:
|
|
|
|
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: php
|
|
|
|
|
|
|
|
<?php
|
|
|
|
namespace Doctrine\Tests\Models\StockExchange;
|
|
|
|
|
|
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @Entity
|
|
|
|
* @Table(name="exchange_markets")
|
|
|
|
*/
|
|
|
|
class Market
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @Id @Column(type="integer") @GeneratedValue
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $id;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @Column(type="string")
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $name;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @OneToMany(targetEntity="Stock", mappedBy="market", indexBy="symbol")
|
|
|
|
* @var Stock[]
|
|
|
|
*/
|
|
|
|
private $stocks;
|
|
|
|
|
|
|
|
public function __construct($name)
|
|
|
|
{
|
|
|
|
$this->name = $name;
|
|
|
|
$this->stocks = new ArrayCollection();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getId()
|
|
|
|
{
|
|
|
|
return $this->id;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getName()
|
|
|
|
{
|
|
|
|
return $this->name;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function addStock(Stock $stock)
|
|
|
|
{
|
|
|
|
$this->stocks[$stock->getSymbol()] = $stock;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getStock($symbol)
|
|
|
|
{
|
|
|
|
if (!isset($this->stocks[$symbol])) {
|
|
|
|
throw new \InvalidArgumentException("Symbol is not traded on this market.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->stocks[$symbol];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getStocks()
|
|
|
|
{
|
|
|
|
return $this->stocks->toArray();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.. 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="Doctrine\Tests\Models\StockExchange\Market">
|
|
|
|
<id name="id" type="integer">
|
|
|
|
<generator strategy="AUTO" />
|
|
|
|
</id>
|
|
|
|
|
|
|
|
<field name="name" type="string"/>
|
|
|
|
|
|
|
|
<one-to-many target-entity="Stock" mapped-by="market" field="stocks" index-by="symbol" />
|
|
|
|
</entity>
|
|
|
|
</doctrine-mapping>
|
|
|
|
|
2011-02-05 15:48:12 +03:00
|
|
|
.. code-block:: yaml
|
2011-02-05 15:43:45 +03:00
|
|
|
|
|
|
|
Doctrine\Tests\Models\StockExchange\Market:
|
|
|
|
type: entity
|
|
|
|
id:
|
|
|
|
id:
|
|
|
|
type: integer
|
|
|
|
generator:
|
|
|
|
strategy: AUTO
|
|
|
|
fields:
|
|
|
|
name:
|
|
|
|
type:string
|
|
|
|
oneToMany:
|
|
|
|
stocks:
|
|
|
|
targetEntity: Stock
|
|
|
|
mappedBy: market
|
|
|
|
indexBy: symbol
|
|
|
|
|
|
|
|
Inside the ``addStock()`` method you can see how we directly set the key of the association to the symbol,
|
|
|
|
so that we can work with the indexed assocation directly after invoking ``addStock()``. Inside ``getStock($symbol)``
|
|
|
|
we pick a stock traded on the particular market by symbol. If this stock doesn't exist an exception is thrown.
|
|
|
|
|
|
|
|
The ``Stock`` entity doesn't contain any special instructions that are new, but for completeness
|
|
|
|
here are the code and mappings for it:
|
|
|
|
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: php
|
|
|
|
|
|
|
|
<?php
|
|
|
|
namespace Doctrine\Tests\Models\StockExchange;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @Entity
|
|
|
|
* @Table(name="exchange_stocks")
|
|
|
|
*/
|
|
|
|
class Stock
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @Id @GeneratedValue @Column(type="integer")
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $id;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For real this column would have to be unique=true. But I want to test behavior of non-unique overrides.
|
|
|
|
*
|
|
|
|
* @Column(type="string", unique=true)
|
|
|
|
*/
|
|
|
|
private $symbol;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @ManyToOne(targetEntity="Market", inversedBy="stocks")
|
|
|
|
* @var Market
|
|
|
|
*/
|
|
|
|
private $market;
|
|
|
|
|
|
|
|
public function __construct($symbol, Market $market)
|
|
|
|
{
|
|
|
|
$this->symbol = $symbol;
|
|
|
|
$this->market = $market;
|
|
|
|
$market->addStock($this);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSymbol()
|
|
|
|
{
|
|
|
|
return $this->symbol;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.. 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="Doctrine\Tests\Models\StockExchange\Stock">
|
|
|
|
<id name="id" type="integer">
|
|
|
|
<generator strategy="AUTO" />
|
|
|
|
</id>
|
|
|
|
|
|
|
|
<field name="symbol" type="string" unique="true" />
|
|
|
|
<many-to-one target-entity="Market" field="market" inversed-by="stocks" />
|
|
|
|
</entity>
|
|
|
|
</doctrine-mapping>
|
|
|
|
|
2011-02-05 15:48:12 +03:00
|
|
|
.. code-block:: yaml
|
2011-02-05 15:43:45 +03:00
|
|
|
|
|
|
|
Doctrine\Tests\Models\StockExchange\Stock:
|
|
|
|
type: entity
|
|
|
|
id:
|
|
|
|
id:
|
|
|
|
type: integer
|
|
|
|
generator:
|
|
|
|
strategy: AUTO
|
|
|
|
fields:
|
|
|
|
symbol:
|
|
|
|
type: string
|
|
|
|
manyToOne:
|
|
|
|
market:
|
|
|
|
targetEntity: Market
|
|
|
|
inversedBy: stocks
|
|
|
|
|
|
|
|
Querying indexed associations
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
Now that we defined the stocks collection to be indexed by symbol we can take a look at some code,
|
|
|
|
that makes use of the indexing.
|
|
|
|
|
|
|
|
First we will populate our database with two example stocks traded on a single market:
|
|
|
|
|
|
|
|
.. code-block:: php
|
|
|
|
|
|
|
|
<?php
|
|
|
|
// $em is the EntityManager
|
|
|
|
|
|
|
|
$market = new Market("Some Exchange");
|
|
|
|
$stock1 = new Stock("AAPL", $market);
|
|
|
|
$stock2 = new Stock("GOOG", $market);
|
|
|
|
|
|
|
|
$em->persist($market);
|
|
|
|
$em->persist($stock1);
|
|
|
|
$em->persist($stock2);
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
This code is not particular interesting since the indexing feature is not yet used. In a new request we could
|
|
|
|
now query for the market:
|
|
|
|
|
|
|
|
.. code-block:: php
|
|
|
|
|
|
|
|
<?php
|
|
|
|
// $em is the EntityManager
|
|
|
|
$marketId = 1;
|
|
|
|
$symbol = "AAPL";
|
|
|
|
|
|
|
|
$market = $em->find("Doctrine\Tests\Models\StockExchange\Market", $marketId);
|
|
|
|
|
|
|
|
// Access the stocks by symbol now:
|
2011-02-05 17:23:39 +03:00
|
|
|
$stock = $market->getStock($symbol);
|
2011-02-05 15:43:45 +03:00
|
|
|
|
|
|
|
echo $stock->getSymbol(); // will print "AAPL"
|
|
|
|
|
|
|
|
The implementation ``Market::addStock()`` in combination with ``indexBy`` allows to access the collection
|
|
|
|
consistently by the Stock symbol. It does not matter if Stock is managed by Doctrine or not.
|
|
|
|
|
|
|
|
The same applies to DQL queries: The ``indexBy`` configuration acts as implicit "INDEX BY" to a join association.
|
|
|
|
|
|
|
|
.. code-block:: php
|
|
|
|
|
|
|
|
<?php
|
|
|
|
// $em is the EntityManager
|
|
|
|
$marketId = 1;
|
|
|
|
$symbol = "AAPL";
|
|
|
|
|
|
|
|
$dql = "SELECT m, s FROM Doctrine\Tests\Models\StockExchange\Market m JOIN m.stocks s WHERE m.id = ?1";
|
|
|
|
$market = $em->createQuery($dql)
|
|
|
|
->setParameter(1, $marketId)
|
|
|
|
->getSingleResult();
|
|
|
|
|
|
|
|
// Access the stocks by symbol now:
|
2011-02-05 17:23:39 +03:00
|
|
|
$stock = $market->getStock($symbol);
|
2011-02-05 15:43:45 +03:00
|
|
|
|
|
|
|
echo $stock->getSymbol(); // will print "AAPL"
|
|
|
|
|
2011-02-05 17:26:58 +03:00
|
|
|
If you want to use ``INDEX BY`` explicitly on an indexed association you are free to do so. Additionally
|
|
|
|
indexed associations also work with the ``Collection::slice()`` functionality, no matter if marked as
|
|
|
|
LAZY or EXTRA_LAZY.
|
|
|
|
|
2011-02-05 15:43:45 +03:00
|
|
|
Outlook into the Future
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
For the inverse side of a many-to-many associations there will be a way to persist the keys and the order
|
2011-02-05 15:48:12 +03:00
|
|
|
as a third and fourth parameter into the join table. This feature is discussed in `DDC-213 <http://www.doctrine-project.org/jira/browse/DDC-213>`_
|
2011-02-05 15:43:45 +03:00
|
|
|
This feature cannot be implemeted for One-To-Many associations, because they are never the owning side.
|
|
|
|
|