Повышение производительности проекта на 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-&gt;getDoctrine()-&gt;getRepository('AcmeDemoBundle:Post')-&gt;findAll();

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

doctrine 2 query

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

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

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

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

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

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

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

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

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

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

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

doctrine 2 queries

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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