Skip to content
This repository has been archived by the owner on Mar 30, 2018. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
Add Working with Indexed Associations Tutorial
  • Loading branch information
beberlei committed Feb 5, 2011
1 parent 7d42497 commit 0f974c5
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 1 deletion.
1 change: 1 addition & 0 deletions en/index.rst
Expand Up @@ -42,6 +42,7 @@ Tutorials
:maxdepth: 1

tutorials/getting-started-xml-edition
tutorials/working-with-indexed-associations

Cookbook
--------
Expand Down
2 changes: 1 addition & 1 deletion en/reference/query-builder.rst
Expand Up @@ -276,7 +276,7 @@ complete list of supported helper methods available:
public function from($from, $alias); // Returns Expr\From instance
// Example - $qb->expr()->leftJoin('u.Phonenumbers', 'p', Expr\Join::ON, 'p.user_id = u.id AND p.country_code = 55');
// Example - $qb->expr()->leftJoin('u. Phonenumbers', 'p', 'ON', $qb->expr()->andx($qb->expr()->eq('p.user_id', 'u.id'), $qb->expr()->eq('p.country_code', '55'));
// Example - $qb->expr()->leftJoin('u.Phonenumbers', 'p', 'ON', $qb->expr()->andx($qb->expr()->eq('p.user_id', 'u.id'), $qb->expr()->eq('p.country_code', '55'));
public function leftJoin($join, $alias, $conditionType = null, $condition = null); // Returns Expr\Join instance
// Example - $qb->expr()->innerJoin('u.Group', 'g', Expr\Join::WITH, 'g.manager_level = 100');
Expand Down
294 changes: 294 additions & 0 deletions en/tutorials/working-with-indexed-assocations.rst
@@ -0,0 +1,294 @@
Working with Indexed Assocations
================================

.. note:
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>
.. code-block:: yml
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>
.. code-block:: yml
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:
$stock = $market->getSymbol($symbol);
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:
$stock = $market->getSymbol($symbol);
echo $stock->getSymbol(); // will print "AAPL"
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
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>`_
This feature cannot be implemeted for One-To-Many associations, because they are never the owning side.

0 comments on commit 0f974c5

Please sign in to comment.