496 lines
14 KiB
Plaintext
496 lines
14 KiB
Plaintext
|
Reference Mapping
|
|||
|
=================
|
|||
|
|
|||
|
This chapter explains how references between documents are mapped with Doctrine.
|
|||
|
|
|||
|
Collections
|
|||
|
-----------
|
|||
|
|
|||
|
Examples of many-valued references in this manual make use of a ``Collection``
|
|||
|
interface and a corresponding ``ArrayCollection`` implementation, which are
|
|||
|
defined in the ``Doctrine\Common\Collections`` namespace. These classes have no
|
|||
|
dependencies on ODM, and can therefore be used within your domain model and
|
|||
|
elsewhere without introducing coupling to the persistence layer.
|
|||
|
|
|||
|
ODM also provides a ``PersistentCollection`` implementation of ``Collection``,
|
|||
|
which incorporates change-tracking functionality; however, this class is
|
|||
|
constructed internally during hydration. As a developer, you should develop with
|
|||
|
the ``Collection`` interface in mind so that your code can operate with any
|
|||
|
implementation.
|
|||
|
|
|||
|
.. note::
|
|||
|
|
|||
|
New in 1.1: you are no longer limited to using ``ArrayCollection`` and can
|
|||
|
freely use your own ``Collection`` implementation. For more details please
|
|||
|
see :doc:`Custom Collections <custom-collections>` chapter.
|
|||
|
|
|||
|
Why are these classes used over PHP arrays? Native arrays cannot be
|
|||
|
transparently extended in PHP, which is necessary for many advanced features
|
|||
|
provided by the ODM. Although PHP does provide various interfaces that allow
|
|||
|
objects to operate like arrays (e.g. ``Traversable``, ``Countable``,
|
|||
|
``ArrayAccess``), and even a concrete implementation in ``ArrayObject``, these
|
|||
|
objects cannot always be used everywhere that a native array is accepted.
|
|||
|
Doctrine's ``Collection`` interface and ``ArrayCollection`` implementation are
|
|||
|
conceptually very similar to ``ArrayObject``, with some slight differences and
|
|||
|
improvements.
|
|||
|
|
|||
|
.. _reference_one:
|
|||
|
|
|||
|
Reference One
|
|||
|
-------------
|
|||
|
|
|||
|
Reference one document:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class Product
|
|||
|
{
|
|||
|
// ...
|
|||
|
|
|||
|
/**
|
|||
|
* @ReferenceOne(targetDocument="Shipping")
|
|||
|
*/
|
|||
|
private $shipping;
|
|||
|
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class Shipping
|
|||
|
{
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<?xml version="1.0" encoding="UTF-8"?>
|
|||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
|||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
|||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
|||
|
<document name="Documents\Product">
|
|||
|
<reference-one field="shipping" target-document="Documents\Shipping" />
|
|||
|
</document>
|
|||
|
</doctrine-mongo-mapping>
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
Product:
|
|||
|
type: document
|
|||
|
referenceOne:
|
|||
|
shipping:
|
|||
|
targetDocument: Documents\Shipping
|
|||
|
|
|||
|
.. _reference_many:
|
|||
|
|
|||
|
Reference Many
|
|||
|
--------------
|
|||
|
|
|||
|
Reference many documents:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class User
|
|||
|
{
|
|||
|
// ...
|
|||
|
|
|||
|
/**
|
|||
|
* @ReferenceMany(targetDocument="Account")
|
|||
|
*/
|
|||
|
private $accounts = array();
|
|||
|
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class Account
|
|||
|
{
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<?xml version="1.0" encoding="UTF-8"?>
|
|||
|
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
|
|||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|||
|
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
|
|||
|
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
|
|||
|
<document name="Documents\Product">
|
|||
|
<reference-many field="accounts" target-document="Documents\Account" />
|
|||
|
</document>
|
|||
|
</doctrine-mongo-mapping>
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
User:
|
|||
|
type: document
|
|||
|
referenceMany:
|
|||
|
accounts:
|
|||
|
targetDocument: Documents\Account
|
|||
|
|
|||
|
.. _reference_mixing_document_types:
|
|||
|
|
|||
|
Mixing Document Types
|
|||
|
---------------------
|
|||
|
|
|||
|
If you want to store different types of documents in references, you can simply
|
|||
|
omit the ``targetDocument`` option:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class User
|
|||
|
{
|
|||
|
// ..
|
|||
|
|
|||
|
/** @ReferenceMany */
|
|||
|
private $favorites = array();
|
|||
|
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<field fieldName="favorites" />
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
referenceMany:
|
|||
|
favorites: ~
|
|||
|
|
|||
|
Now the ``$favorites`` property can store a reference to any type of document!
|
|||
|
The class name will be automatically stored in a field named
|
|||
|
``_doctrine_class_name`` within the `DBRef`_ object.
|
|||
|
|
|||
|
.. note::
|
|||
|
|
|||
|
The MongoDB shell tends to ignore fields other than ``$id`` and ``$ref``
|
|||
|
when displaying `DBRef`_ objects. You can verify the presence of any ``$db``
|
|||
|
and discriminator fields by querying and examining the document with a
|
|||
|
driver. See `SERVER-10777 <https://jira.mongodb.org/browse/SERVER-10777>`_
|
|||
|
for additional discussion on this issue.
|
|||
|
|
|||
|
The name of the field within the DBRef object can be customized via the
|
|||
|
``discriminatorField`` option:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class User
|
|||
|
{
|
|||
|
// ..
|
|||
|
|
|||
|
/**
|
|||
|
* @ReferenceMany(discriminatorField="type")
|
|||
|
*/
|
|||
|
private $favorites = array();
|
|||
|
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<reference-many fieldName="favorites">
|
|||
|
<discriminator-field name="type" />
|
|||
|
</reference-many>
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
referenceMany:
|
|||
|
favorites:
|
|||
|
discriminatorField: type
|
|||
|
|
|||
|
You can also specify a discriminator map to avoid storing the |FQCN|
|
|||
|
in each `DBRef`_ object:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class User
|
|||
|
{
|
|||
|
// ..
|
|||
|
|
|||
|
/**
|
|||
|
* @ReferenceMany(
|
|||
|
* discriminatorMap={
|
|||
|
* "album"="Album",
|
|||
|
* "song"="Song"
|
|||
|
* }
|
|||
|
* )
|
|||
|
*/
|
|||
|
private $favorites = array();
|
|||
|
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<reference-many fieldName="favorites">
|
|||
|
<discriminator-map>
|
|||
|
<discriminator-mapping value="album" class="Documents\Album" />
|
|||
|
<discriminator-mapping value="song" class="Documents\Song" />
|
|||
|
</discriminator-map>
|
|||
|
</reference-many>
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
referenceMany:
|
|||
|
favorites:
|
|||
|
discriminatorMap:
|
|||
|
album: Documents\Album
|
|||
|
song: Documents\Song
|
|||
|
|
|||
|
If you have references without a discriminator value that should be considered
|
|||
|
a certain class, you can optionally specify a default discriminator value:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/** @Document */
|
|||
|
class User
|
|||
|
{
|
|||
|
// ..
|
|||
|
|
|||
|
/**
|
|||
|
* @ReferenceMany(
|
|||
|
* discriminatorMap={
|
|||
|
* "album"="Album",
|
|||
|
* "song"="Song"
|
|||
|
* },
|
|||
|
* defaultDiscriminatorValue="album"
|
|||
|
* )
|
|||
|
*/
|
|||
|
private $favorites = array();
|
|||
|
|
|||
|
// ...
|
|||
|
}
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<reference-many fieldName="favorites">
|
|||
|
<discriminator-map>
|
|||
|
<discriminator-mapping value="album" class="Documents\Album" />
|
|||
|
<discriminator-mapping value="song" class="Documents\Song" />
|
|||
|
</discriminator-map>
|
|||
|
<default-discriminator-value value="album" />
|
|||
|
</reference-many>
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
referenceMany:
|
|||
|
favorites:
|
|||
|
discriminatorMap:
|
|||
|
album: Documents\Album
|
|||
|
song: Documents\Song
|
|||
|
defaultDiscriminatorValue: album
|
|||
|
|
|||
|
.. _storing_references:
|
|||
|
|
|||
|
Storing References
|
|||
|
------------------
|
|||
|
|
|||
|
By default all references are stored as a `DBRef`_ object with the traditional
|
|||
|
``$ref``, ``$id``, and (optionally) ``$db`` fields (in that order). For references to
|
|||
|
documents of a single collection, storing the collection (and database) names for
|
|||
|
each reference may be redundant. You can use simple references to store the
|
|||
|
referenced document's identifier (e.g. ``MongoId``) instead of a `DBRef`_.
|
|||
|
|
|||
|
Example:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/**
|
|||
|
* @ReferenceOne(targetDocument="Profile", storeAs="id")
|
|||
|
*/
|
|||
|
private $profile;
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<reference-one target-document="Documents\Profile", store-as="id" />
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
referenceOne:
|
|||
|
profile:
|
|||
|
storeAs: id
|
|||
|
|
|||
|
Now, the ``profile`` field will only store the ``MongoId`` of the referenced
|
|||
|
Profile document.
|
|||
|
|
|||
|
Simple references reduce the amount of storage used, both for the document
|
|||
|
itself and any indexes on the reference field; however, simple references cannot
|
|||
|
be used with discriminators, since there is no `DBRef`_ object in which to store
|
|||
|
a discriminator value.
|
|||
|
|
|||
|
In addition to saving references as `DBRef`_ with ``$ref``, ``$id``, and ``$db``
|
|||
|
fields and as ``MongoId``, it is possible to save references as `DBRef`_ without
|
|||
|
the ``$db`` field. This solves problems when the database name changes (and also
|
|||
|
reduces the amount of storage used).
|
|||
|
|
|||
|
The ``storeAs`` option has the following possible values:
|
|||
|
|
|||
|
- **dbRefWithDb**: Uses a `DBRef`_ with ``$ref``, ``$id``, and ``$db`` fields (this is the default)
|
|||
|
- **dbRef**: Uses a `DBRef`_ with ``$ref`` and ``$id``
|
|||
|
- **ref**: Uses a custom embedded object with an ``id`` field
|
|||
|
- **id**: Uses the identifier of the referenced object
|
|||
|
|
|||
|
.. note::
|
|||
|
|
|||
|
The ``storeAs=id`` option used to be called a "simple reference". The old syntax is
|
|||
|
still recognized (so using ``simple=true`` will imply ``storeAs=id``).
|
|||
|
|
|||
|
.. note::
|
|||
|
|
|||
|
For backwards compatibility ``storeAs=dbRefWithDb`` is the default, but
|
|||
|
``storeAs=ref`` is the recommended setting.
|
|||
|
|
|||
|
|
|||
|
Cascading Operations
|
|||
|
--------------------
|
|||
|
|
|||
|
By default, Doctrine will not cascade any ``UnitOfWork`` operations to
|
|||
|
referenced documents. You must explicitly enable this functionality:
|
|||
|
|
|||
|
.. configuration-block::
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
/**
|
|||
|
* @ReferenceOne(targetDocument="Profile", cascade={"persist"})
|
|||
|
*/
|
|||
|
private $profile;
|
|||
|
|
|||
|
.. code-block:: xml
|
|||
|
|
|||
|
<reference-one target-document="Documents\Profile">
|
|||
|
<cascade>
|
|||
|
<persist/>
|
|||
|
</cascade>
|
|||
|
</reference-one>
|
|||
|
|
|||
|
.. code-block:: yaml
|
|||
|
|
|||
|
referenceOne:
|
|||
|
profile:
|
|||
|
cascade: [persist]
|
|||
|
|
|||
|
The valid values are:
|
|||
|
|
|||
|
- **all** - cascade all operations by default.
|
|||
|
- **detach** - cascade detach operation to referenced documents.
|
|||
|
- **merge** - cascade merge operation to referenced documents.
|
|||
|
- **refresh** - cascade refresh operation to referenced documents.
|
|||
|
- **remove** - cascade remove operation to referenced documents.
|
|||
|
- **persist** - cascade persist operation to referenced documents.
|
|||
|
|
|||
|
Orphan Removal
|
|||
|
--------------
|
|||
|
|
|||
|
There is another concept of cascading that is relevant only when removing documents
|
|||
|
from collections. If a Document of type ``A`` contains references to privately
|
|||
|
owned Documents ``B`` then if the reference from ``A`` to ``B`` is removed the
|
|||
|
document ``B`` should also be removed, because it is not used anymore.
|
|||
|
|
|||
|
OrphanRemoval works with both reference one and many mapped fields.
|
|||
|
|
|||
|
.. note::
|
|||
|
|
|||
|
When using the ``orphanRemoval=true`` option Doctrine makes the assumption
|
|||
|
that the documents are privately owned and will **NOT** be reused by other documents.
|
|||
|
If you neglect this assumption your documents will get deleted by Doctrine even if
|
|||
|
you assigned the orphaned documents to another one.
|
|||
|
|
|||
|
As a better example consider an Addressbook application where you have Contacts, Addresses
|
|||
|
and StandingData:
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
namespace Addressbook;
|
|||
|
|
|||
|
use Doctrine\Common\Collections\ArrayCollection;
|
|||
|
|
|||
|
/**
|
|||
|
* @Document
|
|||
|
*/
|
|||
|
class Contact
|
|||
|
{
|
|||
|
/** @Id */
|
|||
|
private $id;
|
|||
|
|
|||
|
/** @ReferenceOne(targetDocument="StandingData", orphanRemoval=true) */
|
|||
|
private $standingData;
|
|||
|
|
|||
|
/** @ReferenceMany(targetDocument="Address", mappedBy="contact", orphanRemoval=true) */
|
|||
|
private $addresses;
|
|||
|
|
|||
|
public function __construct()
|
|||
|
{
|
|||
|
$this->addresses = new ArrayCollection();
|
|||
|
}
|
|||
|
|
|||
|
public function newStandingData(StandingData $sd)
|
|||
|
{
|
|||
|
$this->standingData = $sd;
|
|||
|
}
|
|||
|
|
|||
|
public function removeAddress($pos)
|
|||
|
{
|
|||
|
unset($this->addresses[$pos]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
Now two examples of what happens when you remove the references:
|
|||
|
|
|||
|
.. code-block:: php
|
|||
|
|
|||
|
<?php
|
|||
|
|
|||
|
$contact = $dm->find("Addressbook\Contact", $contactId);
|
|||
|
$contact->newStandingData(new StandingData("Firstname", "Lastname", "Street"));
|
|||
|
$contact->removeAddress(1);
|
|||
|
|
|||
|
$dm->flush();
|
|||
|
|
|||
|
In this case you have not only changed the ``Contact`` document itself but
|
|||
|
you have also removed the references for standing data and as well as one
|
|||
|
address reference. When flush is called not only are the references removed
|
|||
|
but both the old standing data and the one address documents are also deleted
|
|||
|
from the database.
|
|||
|
|
|||
|
.. _`DBRef`: https://docs.mongodb.com/manual/reference/database-references/#dbrefs
|
|||
|
.. |FQCN| raw:: html
|
|||
|
<abbr title="Fully-Qualified Class Name">FQCN</abbr>
|