Databases and Doctrine («The Model») (RUS)



Следует признать, что одна из самых обычных и сложных задач для любого приложения сопряжена с хранением и чтением информации внутри и вне базы данных. К счастью, в Symfony интегрирована Doctrine, библиотека, упрощающая обработку информации. В этой главе Вы ознакомитесь с основными положениями Doctrine и узнаете насколько простой может быть работа с базой данных.

Doctrine полностью обособлена от Symfony и использование её не является обязательным. Эта глава полностью посвящена Doctrine ORM, целью которого является отображение объектов в реляционных базах данных (MySQL, PostgreSQL или Microsoft SQL). Если Вы предпочитаете отправлять запрос к базе данных напрямую, то это несложно. Более подробную информацию можно найти в статье “Doctrine’s DBAL Layer. Использование.”

Вы также можете хранить данные в MongoDB используя библиотеку Doctrine ODM. Чтобы болучить более подробную информацию, читайте статью “Использование MongoDB”.

Простой Пример: Product

Самый простой способ понять, как же работает Doctrine – увидеть её в действии. В этом разделе Вы научитесь настраивать Вашу базу данных, создавать объект Product, сохранять его в базе данных и извлекать его обратно.

Код с примером:

Если Вы хотите на практике применить пример, приведённый в данной главе, Вам необходимо создать AcmeStoreBundle следующим образом:

php app/console generate:bundle --namespace=Acme/StoreBundle

Настройка базы данных

Для начала Вам необходимо будет конфигурировать информацию о подключении к базе данных. Как правило, эта информация конфигурируется в app/config/parameters.ini файле:

;app/config/parameters.ini
[parameters]
    database_driver = pdo_mysql
    database_host = localhost
    database_name = test_project
    database_user = root
    database_password = password

При установке Doctrine основной конфигурационный файл ссылается на параметры, определённые в parameters.ini файл:

doctrine:
    dbal:
        driver: %database_driver%
        host: %database_host%
        dbname: %database_name%
        user: %database_user%
        password: %database_password%

Благодаря выделению информации о базе данных в отдельный файл, Вы можете хранить другую версию этого файла на каждом сервере. Вы также можете хранить конфигурацию базы данных (либо другую конфиденциальную информацию) вне Вашего проекта. Например, в конфигурации Apache.

Теперь, когда в Doctrine есть необходимая информация, Вы можете создать базу данных с использованием указанной библиотеки:

php app/console doctrine:database:create

Создание классов в директории Entity

Предположим, Вы создаёте приложение, в котором будут отображаться товары. Даже без Doctrine или баз данных Вы уже знаете, что для отображения этих товаров необходим объект Product. Создайте этот класс в директории Entity Вашего AcmeStoreBundle:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;

class Product
{
    protected $name;

    protected $price;

    protected $description;
}

Класс (часто его называют “объект”) – это основной класс, содержащий данные. Он достаточно прост и помогает выполнять бизнес-требования относительно необходимых товаров в Вашем приложении. Этот класс ещё нельзя сохранить в базе данных, поскольку это всего лишь простой PHP класс.

Изучив положения Doctrine, Вы сможете с её помощью создавать этот класс:

php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Product" --fields="name:string(255) price:float description:text"

Добавление информации об отображении

Doctrine позволяет Вам гораздо более интересным способом работать с базами данных: вместо извлечения строк из таблицы в массив Вы можете хранить в базе данных целостные объекты и извлекать их оттуда. Данный метод работает путём отображения PHP класса в таблице в базе данных, в то время как свойства этого PHP класса отображаются в столбцах таблицы:

Чтобы использовать Doctrine, Вам необходимо всего лишь создать “метаданные”, или конфигурацию, которая будет точно указывать как именно должен отображаться класс Product и его свойства в базе данных. Необходимые метаданные могут быть определены в различном формате, включая YAML, XML. Также их можно определить непосредственно в Product классе посредством комментариев:

Пучок (bundle) может принять только один формат определения метаданных. Например, нельзя совмещать YAML определения метаданных с аннотированными определениями PHP класса.

Annotations:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="product")
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=100)
     */
    protected $name;

    /**
     * @ORM\Column(type="decimal", scale=2)
     */
    protected $price;

    /**
     * @ORM\Column(type="text")
     */
    protected $description;
}

YAML:

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
    type: entity
    table: product
    id:
        id:
            type: integer
            generator: { strategy: AUTO }
    fields:
        name:
            type: string
            length: 100
        price:
            type: decimal
            scale: 2
        description:
            type: text

XML:

<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.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="Acme\StoreBundle\Entity\Product" table="product">
        <id name="id" type="integer" column="id">
            <generator strategy="AUTO" />
        </id>
        <field name="name" column="name" type="string" length="100" />
        <field name="price" column="price" type="decimal" scale="2" />
        <field name="description" column="description" type="text" />
    </entity>
</doctrine-mapping>

Название таблицы может быть назначено Вами, но оно также может определяться автоматически по названию класса.

Doctrine предоставляет Вам широкий выбор различных типов полей, каждый со своими опциями.

Если Вы используете комментарии, Вам необходимо предварять каждый комментарий приставкой ORM\ (ORM\Column(..)), которая не отображается в документации Doctrine. Вам также нужно будет использовать Doctrine\ORM\Mapping в качестве ORM. Данная структура импортирует аннотационную приставку ORM.

Обратите внимание на то, чтобы название и свойства класса не отображались в виде ключевых слов SQL (например, “группа” или “пользователь”). Например, если Вы назвали класс “Group”, то, по умолчанию, название таблицы будет Group, что может привести к SQL ошибке на некоторых “движках”.

Если Вы работаете с другой библиотекой или программой (т.е. Doxygen), использующей комментарии, Вам следует пользоваться пояснением @IgnoreAnnotation, чтобы выделить комментарии, которые будут игнорироваться Symfony.

Например, чробы исключить комментарий @fn, добавьте следующее:

/**
 * @IgnoreAnnotation("fn")
 */
class Product

Создание механизмов получения (Getters) и установки (Setters)

Несмотря на то, что теперь Doctrine способна сохранить объект Product в базе данных, сам класс практически бесполезен. Поскольку Product является обычным PHP классом, Вам необходимо создать механизмы получения и установки (например, getName(), setName()), чтобы получить доступ к свойствам класса (эти данные защищены). К счастью, Doctrine может сделать это с помощью следующей команды:

php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product

Данная команда подтверждает, что необходимые для класса Product механизмы получения и установки были сгенерированы. Сама по себе команда безопасна – Вы можете выполнять её снова и снова. Она будет генерировать отсутствующие механизмы получения и установки (т.е., существующие механизмы не будут заменены).

Вы также можете генерировать все известные классы (т.е. любой класс PHP с информацией об отображении от Doctrine) в пределах отдельного пучка или целого пространства имён:

php app/console doctrine:generate:entities AcmeStoreBundle
php app/console doctrine:generate:entities Acme

Doctrine не различает защищённые и личные свойства; имеете ли Вы механизм получения и установки для отдельного свойства или нет. Эти механизмы генерируются только потому, что Вам они необходимы для взаимодействия с PHP объектом.

Создание таблиц/схемы базы данных

Поскольку теперь у Вас есть готовый клас Product с информацией об отображении, Doctrine точно знает, как сохранить его. Однако, Вы ещё не создали соответствующую таблицу product в базе данных. К счастью, Doctrine может автоматически создать в базе данных все необходимые таблицы для всех известных классов Вашего приложения. Просто выполните:

php app/console doctrine:schema:update --force

Данная команда невероятно мощная. Она проводит сравнение того, как Ваша база данных должна выглядеть (на основании информации об отображении классов) с тем, как она выглядит на деле. Также командой генерируется SQL оператор, необходимый для обновления базы данных до необходимого состояния. Иными словами, если Вы добавите новое свойство с метаданными отображения в Product и выполните эту команду снова, она генерирует оператора “таблица изменений”, который необходим для добавления нового столбца в существующую таблицу products.

Возможность ещё лучше использовать преимущества этого функционала предоставляют миграции, позволяющие генерировать SQL операторов и хранить их в миграционных классах, которые могут запускаться систематически на Вашем сервере для надёжного и безопасного прослеживания и переноса таблиц Вашей базы данных.

Теперь Ваша база данных представляет собой полностью функциональную product таблицу со столбцами, соответствующими указанным Вами метаданным.

Сохранение объектов в базу данных

Теперь, когда у Вас есть объект Product и соответствующая product таблица, можно сохранить имеющуюся информацию в базе данных. Сделать это изнутри контроллера достаточно просто. Добавьте следующую функцию в DefaultController пучка:

 1  // src/Acme/StoreBundle/Controller/DefaultController.php
 2  use Acme\StoreBundle\Entity\Product;
 3  use Symfony\Component\HttpFoundation\Response;
 4  // ...
 5
 6  public function createAction()
 7  {
 8      $product = new Product();
 9      $product->setName('A Foo Bar');
10      $product->setPrice('19.99');
11      $product->setDescription('Lorem ipsum dolor');
12
13      $em = $this->getDoctrine()->getEntityManager();
14      $em->persist($product);
15      $em->flush();
16
17      return new Response('Created product id '.$product->getId());
18  }

Если Вы хотите применить этот пример практически, создайте маршрут, указывающий на это действие.

Рассмотрим этот пример подробнее:

  • lines 8-11 В пределах этих строк Вы конкретизируете и работаете с объектом $produc, проделывая это таким же образом, как и с любым другим PHP объектом;
  • line 13 Эта строка выбирает объектный менеджер Doctrine, который отвечает за процесс сохранения и выборки объектов внутри и вне базы данных;
  • line 14 Функция persist() даёт указание Doctrine “координировать” объект $product. Это ещё не запрос к базе данных (пока).
  • line 15 Когда вводится функция flush(), Doctrine просматривает все координируемые ею объекты, чтобы определить, должны ли они быть сохранены в базе данных или нет. В данном примере объект $product ещё не сохранялся. Таким образом, объектный менеджер выполняет INSERT запрос, а в таблице product создаётся столбец.

Фактически, Doctrine известны все объекты, которыми Вы управляете. Когда Вы запускаете функцию flush(), она подсчитывает общее количество изменений и выполняет наиболее эффективные запросы. Например, если Вы сохраняете 100 Product объектов, а затем запускаете persist(), Doctrine создаст единого оператора и использует его для каждой вставки. Данная модель называется Unit of Work и используется, благодаря своей эффективности и быстродействию.

Во время создания или обновления объектов последовательность действий не меняется. В следующем разделе Вы убедитесь в том, что Doctrine может автоматически выполнить UPDATE запрос, если соответствующая запись уже существует в базе данных.

Doctrine предоставляет библиотеку, позволяющую программно загружать тестируемые данные в Ваш проект (например, “fixture data”).

Выборка объектов из базы данных

Выборка объектов из базы данных производится очень просто. Предположим, Вы настроили маршрут для отображения определённого Product объекта, основываясь на его значении id:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->find($id);

    if (!$product) {
        throw $this->createNotFoundException('No product found for id '.$id);
    }

    // do something, like pass the $product object into a template
}

При запрашивании определённого типа объектов, обычно используется то, что называется “архив” («repository»). Можно сказать, что архив – это PHP класс, единственным предназначением которого является помощь в выборке объектов определённого класса. Вы можете получить доступ к классу объектов в архиве посредством:

$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');

Строка AcmeStoreBundle:Product – это ярлык, который Вы можете использовать в Doctrine вместо полного названия класса или объекта (например, Acme\StoreBundle\Entity\Product). Пока Ваш объект находится в пространстве имён Entity, использование данного ярлыка будет эффективным.

Имея собственный архив, Вы получаете доступ ко всевозможным полезным функциям:

// query by the primary key (usually "id")
$product = $repository->find($id);

// dynamic method names to find based on a column value
$product = $repository->findOneById($id);
$product = $repository->findOneByName('foo');

// find *all* products
$products = $repository->findAll();

// find a group of products based on an arbitrary column value
$products = $repository->findByPrice(19.99);

Вы также можете использовать преимущества функций findBy и findOneBy для выборки объектов, основанной на нескольких условиях:

// query for one product matching be name and price
$product = $repository->findOneBy(array('name' => 'foo', 'price' => 19.99));

// query for all products matching the name, ordered by price
$product = $repository->findBy(
    array('name' => 'foo'),
    array('price' => 'ASC')
);

Во время визуализации веб-страницы Вы можете видеть количество сделанных запросов в правом нижнем углу панели отладки.

После нажатия иконки появляется профайл, отображающий сделанные запросы.

Обновление объекта

После выборки объекта из Doctrine Вы легко можете обновить его. Предположим, у Вас есть маршрут, отображающий id товара для обновления в контроллере:

public function updateAction($id)
{
    $em = $this->getDoctrine()->getEntityManager();
    $product = $em->getRepository('AcmeStoreBundle:Product')->find($id);

    if (!$product) {
        throw $this->createNotFoundException('No product found for id '.$id);
    }

    $product->setName('New product name!');
    $em->flush();

    return $this->redirect($this->generateUrl('homepage'));
}

Обновление объекта происходит всего в три этапа:

1. выборка объекта из Doctrine;

2. изменение объекта;

3. запуск flush() в объектном менеджере

При этом, Вы можете не использовать $em->persist($product). Напомним, что с помощью этой функции Doctrine упорядочивает или “просматривает” объект $product. В данном случае, объект $product упорядочивается при выборке его из Doctrine.

Удаление объекта

Удаление объекта очень похож на процесс, описанный а предыдущей главе. Отличие заключается в использовании remove() функции объектного менеджера:

$em->remove($product);
$em->flush();

Функция remove() уведомляет Doctrine о том, что Вы хотели бы удалить данный объект из базы данных. Однако, запрос DELETE не будет выполнен до тех пор, пока не будет вызвана функция flush().

Запрос к объекту

Вы уже ознакомились с тем, как легко архив позволяет выполнять основные запросы:

$repository->find($id);

$repository->findOneByName('Foo');

Естественно, Doctrine также позволяет Вам создавать более сложные запросы, используя язык DQL (Doctrine Query Language). DQL подобен языку SQL за исключением того, что Вам необходимо представить ситуацию, в которой Вы запрашиваете один или более объектов определённого класса (например, Product) вместо запрашивания строк в таблице (например, product).

Запрос к объекту с использованием DQL

Предположим, Вы хотели бы сделать запрос с отображением товаров, стоящих дороже 19.99, и располагающихся в возрастающем порядке (от самых дешёвых до самых дорогих). В контроллере проделайте следующее:

$em = $this->getDoctrine()->getEntityManager();
$query = $em->createQuery(
    'SELECT p FROM AcmeStoreBundle:Product p WHERE p.price > :price ORDER BY p.price ASC'
)->setParameter('price', '19.99');

$products = $query->getResult();

Если Вы уверенно работаете с SQL, то с DQL затруднений у Вас не возникнет. Самая большая разница заключается в том, что Вам необходимо будет работать с понятием “объекты” вместо строк в базе данных. Поэтому, Вы выделяете from AcmeStoreBundle:Product и обозначаете его псевдонимом p.

Функция getResult() возвращает массив результатов. Если Вы делаете запрос только к одному объекту, то можете использовать функцию getSingleResult() :

$product = $query->getSingleResult();

Функция getSingleResult() выдаёт исключение Doctrine\ORM\NoResultException в случае отсутствия результатовe, и Doctrine\ORM\NonUniqueResultException в случае возвращения более чем одного результата. При использовании этой функции Вы можете “завернуть” её в try-catch блок и удостовериться в том, что возвращается только один результат (в том случае, когда Вы запрашиваете нечто такое, что может вернуть более одного результата):

$query = $em->createQuery('SELECT ....')
    ->setMaxResults(1);

try {
    $product = $query->getSingleResult();
} catch (\Doctrine\Orm\NoResultException $e) {
    $product = null;
}
// ...

Синтаксис DQL необычайно мощный и позволяет объединять группы, объекты, и т.д.

Установка параметров

Обратите внимание на функцию setParameter(). Работая с Doctrine, всегда полезно назначать какие-либо внешние значения (например, «placeholders»), что и было проделано в вышеупомянутом запросе:

... WHERE p.price > :price ...

Затем Вы можете назначить значение “заполнителя” price, используя функцию setParameter():

->setParameter('price', '19.99')

Во избежание SQL инъекций следует всегда использовать параметры вместо размещения значений непосредственно в строке запроса. Если Вы используете несколько параметров, можно сразу же установить их значение, используя функцию setParameters():

->setParameters(array(
    'price' => '19.99',
    'name'  => 'Foo',
))

Использование Doctrine’s Query Builder

Вместо непосредственного написания запросов Вы можете использовать Doctrine’s QueryBuilder, который выполнит эту задачу с помощью удобного, объектно-ориентированного интерфейса. При использовании IDE, Вы можете воспользоваться автозаполнением при введении названий функций. В контроллере:

$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');

$query = $repository->createQueryBuilder('p')
    ->where('p.price > :price')
    ->setParameter('price', '19.99')
    ->orderBy('p.price', 'ASC')
    ->getQuery();

$products = $query->getResult();

QueryBuilder содержит в себе все функции, необходимые для создания запроса. При выполнении функции getQuery() генератор запросов возвращает обычный объект, который является тем самым запросом, который был создан Вами в предыдущем разделе.

Специальные репозитарные классы

В предыдущих разделах Вы начали создавать и использовать в контроллере более-менее сложные запросы. Чтобы выделить, протестировать и повторно использовать эти запросы, полезно создать специальный репозитарный класс для Ваших объектов, а также добавить функции с логической схемой запросов.

Чтобы сделать это, задайте название репозитарного класса в отображении.

Annotations:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="Acme\StoreBundle\Repository\ProductRepository")
 */
class Product
{
    //...
}

YAML:

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
    type: entity
    repositoryClass: Acme\StoreBundle\Repository\ProductRepository
    # ...

XML:

<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>

    <entity name="Acme\StoreBundle\Entity\Product"
            repository-class="Acme\StoreBundle\Repository\ProductRepository">
            <!-- ... -->
    </entity>
</doctrine-mapping>

Doctrine способна генерировать репозитарный класс с помощью тех же команд, которые использовались ранее для создания недостающих механизмов получения и установки:

php app/console doctrine:generate:entities Acme

Далее, в новосозданный репозитарный класс добавим функцию findAllOrderedByName(). Эта функция выполнит запрос ко всем объектам Product, расположенным в алфавитном порядке.

// src/Acme/StoreBundle/Repository/ProductRepository.php
namespace Acme\StoreBundle\Repository;

use Doctrine\ORM\EntityRepository;

class ProductRepository extends EntityRepository
{
    public function findAllOrderedByName()
    {
        return $this->getEntityManager()
            ->createQuery('SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC')
            ->getResult();
    }
}

The entity manager can be accessed via $this->getEntityManager() from inside the repository.

Для Вашего репозитория Вы можете использовать эту новую функцию в качестве поисковых функций по умолчанию:

$em = $this->getDoctrine()->getEntityManager();
$products = $em->getRepository('AcmeStoreBundle:Product')
            ->findAllOrderedByName();

Несмотря на использование специального репозитарного класса, Вам также доступны стандартные поисковые функции, такие как find() и findAll().

Связь/Ассоциации между объектами

Предположим, что все товары в Вашем приложении принадлежать одной “категории”. В таком случае, Вам потребуется объект Category, а также способ связать между собой объекты Product и Category. Начните с создания объекта Category. Поскольку Вам известно о том, что класс сохранятся с помощью Doctrine, Вы можете доверить ей создание самого класса.

php app/console doctrine:generate:entity --entity="AcmeStoreBundle:Category" --fields="name:string(255)"

Эта задача создаст объект Category, включая поля id и name, а также сопутствующие механизмы получения и установки.

Метаданные отображения взаимосвязи

Чтобы связать объекты Category и Product, начните с создания свойства products в классе Category:

// src/Acme/StoreBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;

class Category
{
    // ...

    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
     */
    protected $products;

    public function __construct()
    {
        $this->products = new ArrayCollection();
    }
}

Поскольку объект Category связан с множеством объектов Product, то для содержания данных объектов добавляется свойство массива products. Опять же, это делается не потому, что того требует Doctrine, а потому, что в приложении каждая Category должна содержать массив Product объектов.

Код в составе __construct() функции очень важен, так как Doctrine необходимо, чтобы свойство $products было ArrayCollection объектом. Этот объект выглядит и действует почти точно так же, как и массив, но обладает большей гибкостью. Не беспокойтесь, если данная схема покажется Вам неудобной. Просто представьте, что это array и Вы снова почувствуете себя в своей тарелке.

Далее, поскольку каждый Product класс может соотноситься только с одним Category объектом, Вам захочется добавить свойство $category в Product класс:

// src/Acme/StoreBundle/Entity/Product.php
// ...

class Product
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    protected $category;
}

Наконец, когда Вы добавили новые свойства в классы Category и Product, следует указать Doctrine на необходимость создания недостающих механизмов получения и установки:

php app/console doctrine:generate:entities Acme

Отвлечёмся пока от метаданных Doctrine. Теперь у Вас есть два класса – Category и Product с естественным типом взаимоотношения “один-множество”. Класс Category содержит массив Product объектов, а Product объект может содержать один Category объект. Иными словами, Вы создали классы в соответствии с Вашими требованиями. Факт, что данные должны сохраняться в базу данных, является вторичным.

Теперь обратим внимание на метаданные над свойством $category класса Product. Информация, которая здесь содержится, указывает doctrine на сопутствующий класс Category, а также на необходимость хранить id категории в поле category_id таблицы product. Иными словами, объект Category будет храниться в свойстве $category, однако, Doctrine будет сохранять взаимосвязь путём хранения id значения категории в столбце category_id таблицы product.

Метаданные над свойством $products объекта Category менее важны. Они служат для того, чтобы указывать Doctrine на необходимость просматривать свойство Product.category, позволяющее выяснить как отображается взаимосвязь.

Перед тем, как продолжить, удостоверьтесь в том, что Doctrine добавила новую category таблицу, столбец product.category_id, и внешний ключ:

php app/console doctrine:schema:update --force

Эту задачу следует выполнять только для процесса разработки.

Сохраниение связанных объектов

Рассмотрим код в действии. В контроллере:

// ...
use Acme\StoreBundle\Entity\Category;
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
// ...

class DefaultController extends Controller
{
    public function createProductAction()
    {
        $category = new Category();
        $category->setName('Main Products');

        $product = new Product();
        $product->setName('Foo');
        $product->setPrice(19.99);
        // relate this product to the category
        $product->setCategory($category);

        $em = $this->getDoctrine()->getEntityManager();
        $em->persist($category);
        $em->persist($product);
        $em->flush();

        return new Response(
            'Created product id: '.$product->getId().' and category id: '.$category->getId()
        );
    }
}

Теперь добавляем одну строку в таблицы category и product. Столбец product.category_id для товара устанавливается для любого id новой категории. Doctrine помогает сохранить эту взаимосвязь.

Выборка взаимосвязанных объектов

Если Вам нужно выбрать взаимодействующие объекты, порядок действий останется неизменным: вначале выбирается $product объект, а затем – связанная с ним Category:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->find($id);

    $categoryName = $product->getCategory()->getName();

    // ...
}

В данном примере, Ваш первый запрос к Product объекту основывается на id самого товара. В результате, запрашиваются только данные товара и сам объект $product “разбавляется” этими данными. Позже, при вызове $product->getCategory()->getName(), Doctrine делает второй запрос в поисках категории, связанной с данным товаром. Таким образом составляется и возвращается $category объект.

Важен то, что Вы легко можете получить доступ к категории, связанной с товаром, однако данные о категории не будут получены до тех пор, пока не будет сделан запрос к категории (т.н. “ленивая загрузка”)

Вы также можете сделать запрос в другом направлении:

public function showProductAction($id)
{
    $category = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Category')
        ->find($id);

    $products = $category->getProducts();

    // ...
}

В данном случае происходит та же ситуация: сначала Вы запрашиваете только Category объект, а затем Doctrine делает второй запрос для получения соответствующего Product объекта, но только в том случае, когда этот объект Вам необходим (т.е. когда вызывается функция ->getProducts()). Переменная $products представляет собой массив всех Product объектов, связанных с данным Category объектом посредством значения category_id.

Взаимосвязь и замещающие классы

Уже упоминавшаяся “ленивая загрузка” возможна благодаря тому, что Doctrine, в случае необходимости, возвращает “объект-заместитель” вместо реального объекта. Рассмотрим вышеприведённый пример ещё раз:

$product = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product')
    ->find($id);

$category = $product->getCategory();

// prints "Proxies\AcmeStoreBundleEntityCategoryProxy"
echo get_class($category);

Данный объект-заместитель детализирует настоящий Category объект и выглядит, и действует точно так же, как и последний. Разница заключается в том, что, используя объекты-заместители, Doctrine может отложить запрашивание реальных Category данных до тех пор, пока они Вам действительно не понадобятся (например, пока Вы не вызовете $category->getName()).

Замещающие классы генерируются Doctrine и хранятся в директории кэша. Хотя Вы, вероятно, никогда даже не замечали, что Ваш $category объект на самом деле является “заместителем”, стоит помнить о подобном классе.

Далее, после получения всех данных категории и товара (с помощью объединения (join)), Doctrine возвратит настоящий Category объект, поскольку “ленивая загрузка” уже не понадобится.

Объединение в взаимосвязанные записи

В приведённых выше примерах были использованы два запроса – один – к оригинальному объекту (например, Category), другой – к соответствующему(-им) объекту(-ам) (например, Product).

Помните, Вы можете видеть все сделанные запросы на панели отладки.

Конечно, если Вы знаете заранее, что Вам понадобится доступ к обоим объектам, то можете заменить второй запрос объединением в оригинальном запросе. Добавьте следующую функцию в ProductRepository класс:

// src/Acme/StoreBundle/Repository/ProductRepository.php

public function findOneByIdJoinedToCategory($id)
{
    $query = $this->getEntityManager()
        ->createQuery('
            SELECT p, c FROM AcmeStoreBundle:Product p
            JOIN p.category c
            WHERE p.id = :id'
        )->setParameter('id', $id);

    try {
        return $query->getSingleResult();
    } catch (\Doctrine\ORM\NoResultException $e) {
        return null;
    }
}

Теперь Вы можете использовать данную функцию в контроллере для выполнения единого запроса к Product объекту и соответствующей ему Category:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->findOneByIdJoinedToCategory($id);

    $category = $product->getCategory();

    // ...
}

Дополнительная информация об ассоциациях

Этот раздел был введением к общему типу взаимосвязи объектов – типу “один – множество”.

Если Вы используете комментарии, следует предварять их приставкой ORM\ (например, ORM\OneToMany), которая не отображается в документации Doctrine. Вам также потребуется использовать предписание Doctrine\ORM\Mapping, импортирующее приставку ORM.

Настройка

Doctrine достаточно легко настраивается, хотя большинство её опций вряд ли когда-либо заинтересует Вас.

Отзывание жизненного цикла

Иногда Вам бывает необходимо предпринять некоторые действия непосредственно перед или после вставки, обновления или удаления объекта. Действия такого типа известны под названием “отзывание жизненного цикла”, поскольку они отзывают функции, которые Вам необходимо выполнить на различных этапах жизненного цикла объекта (например, когда объект вставлен, обновлён, удалён, и т.д.).

Если Вы используете комментарии для метаданных, начните с включения отзывания жизненного цикла. Данное включение необязательно, если Вы используете YAML или XML для маппинга:

/**
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks()
 */
class Product
{
    // ...
}

Теперь Вы можете дать указание Doctrine на выполнение операций в каждом из доступных этапов жизненного цикла. Предположим, ВЫ хотите разместить столбец created в текущих данных, но только после того, как объект будет сохранён (например, после вставки):

Annotations:

/**
 * @ORM\prePersist
 */
public function setCreatedValue()
{
    $this->created = new \DateTime();
}

YAML:

# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
    type: entity
    # ...
    lifecycleCallbacks:
        prePersist: [ setCreatedValue ]

XML:

<!-- src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.xml -->
<!-- ... -->
<doctrine-mapping>

    <entity name="Acme\StoreBundle\Entity\Product">
            <!-- ... -->
            <lifecycle-callbacks>
                <lifecycle-callback type="prePersist" method="setCreatedValue" />
            </lifecycle-callbacks>
    </entity>
</doctrine-mapping>

Вышеприведённый пример показывает, что Вы создали и отмаппили свойство created (не показано здесь).

Теперь, непосредственно перед сохранением объекта, Doctrine автоматически вызовет необходимую функцию и дата поля created будет изменена на текущую.

Данную процедуру можно повторить для любого события жизненного цикла, включая:

  • preRemove
  • postRemove
  • prePersist
  • postPersist
  • preUpdate
  • postUpdate
  • postLoad
  • loadClassMetadata

Отзывание жизненного цикла и приёмники событий

Обратите внимание, что функция setCreatedValue() не принимает параметры. Она всегда относится к отзыванию жизненного цикла и является преднамеренной: отзывание жизненного цикла включает в себя простые функции, связанные с внутренней трансформацией данных объекта (например, установка созданного/обновлённого поля, генерирование значения литой строки).

Если Вам необходимо выполнить нечто более трудоёмкое – выполнить регистрацию или отправить письмо, то следует зарегистрировать какой-либо внешний класс в качестве приёмника событий или пользователя, и предоставить ему доступ к необходимым Вам ресурсам.

Расширения Doctrine: Timestampable, Sluggable и т.д.

Doctrine является достаточно гибкой библиотекой и предусматривает некоторые сторонние расширения, позволяющие легко выполнять повторяющиеся и обычные задачи с Вашими объектами. К таковым относятся: Sluggable, Timestampable, Loggable, Translatable и Tree.

Справочник по типам полей в Doctrine

В Doctrine Вам доступно большое количество типов полей. Каждый из них отображает в специальной колонке какой-либо тип PHP данных любой используемой Вами базе данных. В Doctrine поддерживаются следующие типы:

  • Строки (Strings)
    • string (используется для коротких строк)
    • text (используется для длинных строк)
  • Числа (Numbers)
    • integer
    • smallint
    • bigint
    • decimal
    • float
  • Даты и Временные Периоды (Dates and Times) (в PHP используйте объект DateTime для этих полей)
    • date
    • time
    • datetime
  • Другие типы
    • boolean
    • object (преобразовывается в последовательный режим и хранится в поле CLOB)
    • array (преобразовывается в последовательный режим и хранится в поле CLOB)

Опции поля

Каждое поле может иметь набор применимых к нему опций. Доступные опции включают в себя: type (по умолчанию используется string), name, length, unique и nullable. Приведём несколько примеров комментариев:

/**
 * A string field with length 255 that cannot be null
 * (reflecting the default values for the "type", "length" and *nullable* options)
 *
 * @ORM\Column()
 */
protected $name;

/**
 * A string field of length 150 that persists to an "email_address" column
 * and has a unique index.
 *
 * @ORM\Column(name="email_address", unique="true", length="150")
 */
protected $email;

Консольные команды

Doctrine2 ORM предлагает несколько консольных команд в пространстве имён doctrine. Чтобы просмотреть список команд, Вы можете запустить консоль без каких-либо параметров:

php app/console

Появится список доступных команд, многие из которых начинаются приставкой doctrine:. Вы можете получить более подробную информацию о любой из этих команд (как и о любой команде Symfony), запустив команду help. Например, чтобы узнать подробнее о задаче doctrine:database:create, выполните:

php app/console help doctrine:database:create

Вот некоторые примечательные и интересные задачи:

  • doctrine:ensure-production-settings – проверяет уровень эффективности настройки текущей среды для работы. Эту команду всегда следует выполнять в среде prod:
    php app/console doctrine:ensure-production-settings --env=prod
  • doctrine:mapping:import – позволяет Doctrine анализировать существующую базу данных и создавать информацию отображения.
  • doctrine:mapping:info – сообщает Вам всю информацию об объектах в Doctrine, а также уведомляет о возможных основных ошибках в маппинге.
  • doctrine:query:dql и doctrine:query:sql – позволяет Вам выполнять DQL или SQL запросы непосредственно из командной строки.

Чтобы загружать вспомогательные устройства, Вам необходимо будет установить комплекс DoctrineFixturesBundle.

Резюме

С помощью Doctrine Вы можеть сфокусировать внимание на объектах и на их пользе для Вашего приложения, а также позаботиться о живучести базы данных. Всё это возможно благодаря тому, что Doctrine позволяет Вам использовать любой PHP объект для содержания данных и полагается на информацию метаданных маппинга для отображения данных объекта в отдельной таблице базы данных.

Хотя Doctrine использует простую онцепцию, её невероятная мощь позволяет Вам создавать сложные запросы и предпринимать задачи, позволяющие выполнять различные действия, поскольку объекты полностью вырабатывают жизненный цикл.



Похожие записи:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>