<?php #[ORM\Entity, ORM\Table(name: 'item_price')] class ItemPrice { #[ORM\Id, ORM\Column(type: 'integer'), ORM\GeneratedValue] private int $id; #[ ORM\ManyToOne(targetEntity: Item::class, inversedBy: 'prices'), ORM\JoinColumn(name: 'item_id', referencedColumnName: 'id') ] private Item $item; #[ORM\Column(type: 'string')] private string $amount; #[ORM\Column(type: 'string')] private string $currency; }
Одним из нововведений PHP 8.0 являются атрибуты. Атрибуты содержат метадату для классов, полей, функций; которая доступна через Reflection API. Казалось бы, то же самое, что и аннотации, тогда зачем обращать внимание на эту фичу?
Все отличие в структурированности. Аннотации являются простой строкой phpDoc, а, следовательно, разработчику нужно использовать какой-то механизм для их обработки, чтобы извлечь нужную информацию.
Появляется необходимость парсинга в рантайме. Насколько это плохо? Рассмотрим, как это влияет на производительность на примере Doctrine.
Doctrine, как любая ORM, широко использует метадату для своей работы. В частности — маппинги бизнес-сущностей на таблицы в БД. Есть разные реализации способа хранения метадаты:
-
Драйвер XML
-
Драйвер YAML
-
Статический PHP драйвер
-
PHP драйвер
-
Драйвер аннотаций
-
Драйвер атрибутов
В тестах рассмотрим аннотации и атрибуты. Дополнительно посмотрим на статический и обычный PHP драйвер, чтобы сравнить, насколько предыдущие способы уступают ручной инициализации метадаты. Не забудем взглянуть на драйвера аннотаций с кэшом на основе APCu и Redis с целью понять, решают ли они возможную проблему производительности.
Пример использования драйвера аннотаций
<?php /** * @ORM\Entity * @ORM\Table(name="item_price") */ class ItemPrice { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue */ private int $id; /** * @ORM\ManyToOne(targetEntity="Item", inversedBy="prices") * @ORM\JoinColumn(name="item_id", referencedColumnName="id") */ private Item $item; /** * @ORM\Column(type="string") */ private string $amount; /** * @ORM\Column(type="string") */ private string $currency; }
Пример использования статического PHP драйвера
<?php class ItemPrice { public static function loadMetadata(ClassMetadata $metadata): void { $metadata->setPrimaryTable([ 'table' => 'item_price', ]); $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); $metadata->mapField([ 'fieldName' => 'id', 'type' => 'integer', 'id' => true, ]); $metadata->mapManyToOne([ 'fieldName' => 'item', 'joinColumns' => [ [ 'name' => 'item_id', 'referencedColumnName' => 'id', ], ], 'inversedBy' => 'prices', 'targetEntity' => Item::class, ]); $metadata->mapField([ 'fieldName' => 'amount', 'type' => 'string', ]); $metadata->mapField([ 'fieldName' => 'currency', 'type' => 'string', ]); } private int $id; private Item $item; private string $amount; private string $currency; }
В качестве теста я 10 минут 1 активным пользователем выполнял запросы на получение метадаты всех классов. Использовалась связка Nginx + PHP-fpm с включенным opcache. Исходники доступны здесь.
|
Драйвер |
Всего запросов |
Медиана |
Минимум |
95-перцентиль |
|
35044 |
16.81ms |
13.8ms |
19.16ms |
|
|
106016 |
5.5ms |
4.02ms |
6.64ms |
|
|
114089 |
5.08ms |
3.63ms |
6.23ms |
|
|
98042 |
5.48ms |
3.76ms |
7.47ms |
|
|
Redis Cache |
36057 |
16.2ms |
11.67ms |
19.94ms |
|
APCu Cache |
38943 |
15.06ms |
10.83ms |
18.51ms |
Атрибуты показали неплохую производительность на уровне драйверов, использующих PHP-код. Разницу между AttributeDriver, StaticPHPDriver и PHPDriver, думаю, можно считать погрешностью. Главное, что можно увидеть по результатам тестирования — аннотации в 3 раза медленнее! Интересен так же тот факт, что кэширование не всегда может помочь ускорить приложение — в Doctrine метадата для каждого класса кэшируется отдельной записью. Это приводит к тому, что ее выгрузка и десериализация для каждого класса в отдельности выходит ненамного быстрее.
Если вы ищите способ оптимизировать ваше legacy приложение, и все запросы к БД уже давно проверены, возможно аннотации контроллеров и сущностей ORM — ваша следующая цель.
Тут можно столкнуться с проблемой: legacy — это когда куча кода, а теперь его нужно весь переписывать? Эту проблему можно решить с помощью еще одного варианта, который вы могли видеть в результатах бенчмарка — кодогенерации:
|
Драйвер |
Всего запросов |
Медиана |
Минимум |
95-перцентиль |
|
102124 |
5.59ms |
3.7ms |
7.38ms |
Этот вариант позволяет продолжать использовать аннотации, не переписывая весь проект, при этом получая бонусы производительности — ведь аннотации генерируются в код, а код попадает в opcache!
Резюмируя:
-
Стоит ли использовать в новых приложениях атрибуты вместо аннотаций везде, где это представляется возможным? Однозначно да.
-
Нужно ли переписывать аннотации в legacy проекте на атрибуты? Зависит от нефункциональных требований, возможно вам поможет кодогенерация. Например, подобный скрипт мы используем у себя на продакшене. (Еще рабочим вариантом может оказаться автоматический рефакторинг средствами rector)
Надеюсь, статья была полезна и познакомила вас с ещё одним преимуществом атрибутов перед аннотациями.
ссылка на оригинал статьи https://habr.com/ru/post/686796/
Добавить комментарий