Повышение производительности проекта на Symfony2 c Doctrine2 ORM

Повышение производительности проекта на Symfony2 c Doctrine2 ORM

Я уже давно намеревался написать эту статью, но все никак не доходили руки. Ну вот, я собрался с мыслями и сделал это. Значит, о чем пойдет речь... Я поделюсь некоторыми приемами работы с Doctrine2 ORM, совершим, так сказать, повышение производительности сайта на Symfony 2 (точнее, любого сайта, который использует Doctrine2 ORM). Как наглядное пособие, я создал проект и выложил его на GitHub, так что теперь любой желающий может проверить мои слова в действии.

1. Загрузка всех необходимых связей

Я считаю этот прием наиболее эффективным и поэтому поставил его на первое место.

Скажем, у нас есть 5 авторов и их 100 постов, и нам необходимо вывести все посты с именами авторов на страницу.

Шаблон:

    {% for post in posts %}
    <article>
        <h2>{{ post.title }}</h2>
        <p>Author: {{ post.author.firstName }} {{ post.author.lastName }} created at {{ post.createdAt | date('d-m-Y H:i') }}</p>
        <p>{{ post.text }}</p>
    </article>
    {% endfor %}

Что может быть проще, когда нужна оптимизация работы Symfony 2, скажете вы и сделаете что-то похожее на это:

$posts = $this->getDoctrine()->getRepository('AcmeDemoBundle:Post')->findAll();

Но если взглянуть в профайлер (Symfony Profiler Toolbar), то можно увидеть такую печальную информацию:

doctrine 2 query

Откуда же столько запросов к базе данных, если мы сделали только один запрос?

Все дело в том, что Doctrine2 ORM не загружает по умолчанию сущности из связей, и, когда мы обращаемся к ним, происходит новый запрос на получение этих данных. Вот поэтому у нас еще 5 запросов на получение авторов постов, которые никак не способствуют повышению производительности сайта на Symfony 2.

Для того, чтобы оптимизировать запрос, нужно воспользоваться JOINом и добавить нужные сущности в выборку. Вот оптимизированный код метода репозитория на получение списка постов.

/**
* Find all posts with authors
*
* @return array | Post[]
*/
public function findAllPostsAndAuthors()
{
    $qb = $this->createQueryBuilder('p');
    $qb->addSelect('a')
        ->innerJoin('p.author', 'a');
 
    return $qb->getQuery()->getResult();
}

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

Повышение производительности проекта на Symfony2 c Doctrine2 ORM

Теперь у нас остался только 1 запрос, время выполнения которого уменьшилось по сравнению с предыдущим вариантом и произошло повышение производительности Doctrine 2 orm.

2. Обновление нескольких сущностей запросом

Допустим, нам теперь нужно обновить дату создания всех наших постов. Часто это делают получением всех постов из базы и потом в цикле обновляют каждую сущность в отдельности:

$newCreatedAt = new \DateTime();
$posts = $this->getDoctrine()->getRepository('AcmeDemoBundle:Post')->findAll();
 
/** @var Post $post */
foreach ($posts as $post) {
    $post->setCreatedAt($newCreatedAt);
}
$this->getDoctrine()->getManager()->flush();

В результате чего имеем следующие значения в профайлере после обновления 100 постов:

doctrine 2 queries

Как видно, было выполнено аж 103 запроса к базе данных. Даже профайлер пометил их желтым, что уже должно вас насторожить и заставить подумать об оптимизации.

Что же привело к такому количеству запросов? Из-за того, что мы обновляли сущности в цикле, мы получили большое количество UPDATE запросов в базу данных.

Оптимизация работы Doctrine2 ORM. Нам нужно всего-навсего написать один запрос для обновления сразу всех записей в базе данных.

/**
* Update created date for all posts
*
* @param \DateTime $newCreatedAt
*
* @return int
*/
public function updateCreatedAtForAllPosts(\DateTime $newCreatedAt)
{
    $qb = $this->createQueryBuilder('p');
    $qb->update()
        ->set('p.createdAt', ':newCreatedAt')
        ->setParameter('newCreatedAt', $newCreatedAt);
 
    return $qb->getQuery()->execute();
}

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

3. Отказ от гидрации

Что такое гидрация? Гидрация — преобразование массива в объект и обратно.

Гидрация является самым затратным процессом по времени и по памяти в ORM.

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

$posts = $this->createQueryBuilder('p')->getQuery()->getArrayResult();

Подробнее узнать о способах гидрации можно из документации Doctrine2 ORM.

4. Использование Reference Proxies

Допустим, нам нужно добавить в пост связь с автором, и у нас есть ID этого автора. В большинстве случаев это выполняют получением сущности автора из базы данных по его ID и затем просто устанавливают связь автора с постом:

$author = $this->getDoctrine()->getRepository('AcmeDemoBundle:Author')->find($autorId);
 
$post = new Post();
$post->setAuthor($author);

Как оказалось, получение автора из базы данных — это совершенно лишний запрос, от которого очень легко избавиться. Для это у Doctrine2 есть Reference Proxies. Вот как это работает:

$em = $this->getDoctrine()->getManager();
 
$post = new Post();
$post->setAuthor($em->getReference('Acme\DemoBundle\Entity\Author', $authorId));

Таким образом мы избавились от лишнего запроса и выполнили связь, имея только ID автора.

5. Использование Symfony Profiler Toolbar

Несмотря на то, что это последний пункт, этот совет является не менее важным. Постоянный контроль того, что происходит в профайлере, значительно поможет вам разрабатывать эффективные проекты на Symfony2. Благодаря профайлеру вы всегда будете держать «руку на пульсе» ваших запросов к базе данных и сможете вовремя выполнять оптимизацию работы Symfony 2 для увеличения производительности вашего проекта.

Подробнее ознакомиться с архитектурой Doctrine 2 вы можете здесь.

Stfalcon.com приглашает вас к сотрудничеству!