…или правильная работа с коллекциями.

Хочу рассказать вам об ошибках, которые видел практически на каждом проекте на Magento у которого были проблемы с производительностью. Работая с Magento, мне иногда приходится проводить аудит чужого кода. Поэтому я бы хотел поделиться с вами опытом, который поможет улучшить производительность ваших сайтов и избежать ошибок в дальнейшем.

В этой статье рассказано о Magento 1.*, но описанное так же подходит и для Magento 2.*.

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

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*');
foreach ($collection as $product) {
    $product = $product->load($product->getId());
    $temp[] = $product->getSku();
}
Неправильно

вместо

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('sku');
foreach ($collection as $product) {
    $temp[] = $product->getSku();
}
Правильно

Причины такого очень просты:

  1. После загрузки нет необходимых атрибутов
  2. Так делают «программисты» в интернете
  3. Загрузка лишних атрибутов по принципу «хуже не будет»

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

  1. Eav/Flat таблицы
  2. Cache
  3. Правильная работа с коллекциями

И конечно же выводы.



EAV/Flat таблицы


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

В Magento к EAV сущностям относятся: продукты, категории, кастомеры и кастомер адреса. Сами же атрибуты хранятся в eav_attribute таблице.

Всего типов значений атрибутов в Magento 5: text, varchar, int, decimal и datetime. Есть еще 1 тип – static, он отличается от остальных 5ти тем, что находится в таблице с сущностью.

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

Такое хранение значений позволяет иметь достаточно просто реализуемые атрибут сеты ( когда каждая сущность может иметь свой атрибут или не иметь его вовсе), добавление нового атрибута это всего лишь еще 1 строчка в БД. Добавил новое значение для 1 атрибута для другого стора – новая строчка в таблице значений этого атрибута.

Как это хранится в БД
Entity:
Product – catalog_product_entity,
Category – catalog_category_entity,
Customer – customer_entity,
Customer address – customer_address_entity

Attribute:
eav_attribute
catalog_eav_attribute
customer_eav_attribute

Value:
*_text
*_varchar
*_int
*_decimal
*_datetime

Flat — это привычный нам всем подход, где все лежит в 1 месте и никакие дополнительные таблицы нам не нужны, чтобы получить товар и все его атрибуты без лишней работы – SELECT * FROM табличка WHERE id = какой то ид и все.

Из EAV сущностей, Flat представление можно использовать только для категорий и для товаров.

Как это хранится в БД
Product:
catalog_product_flat_1 // *_N store_view
Category:
catalog_category_flat_1 // *_N store_view

Для того, чтобы включить атрибут во Flat таблицу и вообще включить использование Flat таблиц необходимо проделать следующее
В админке Catalog > Attributes > Manage attributes

Magento добавит атрибут во Flat таблицу если у атрибута выставлено 1 из ниже указанных значений.



В админке System > Configuration > Catalog

Magento будет использовать Flat таблицы для сущностей указанных ниже.



Обратите внимание на следующие факты:

  1. Flat таблицы используются ТОЛЬКО на страницах категорий, списке продуктов в составе Group product, да и вообще везде, где используется коллекция. Они не используются на странице товаров, в админке, при использовании метода load у модели.
  2. После включения Flat таблиц необходимо произвести реиндексацию, иначе Magento будет и дальше использовать только EAV таблицы
  3. После включения Flat таблиц Magento все равно продолжает использовать EAV, но так же начинает копировать изменения во Flat таблицу при сохранении изменений

Зачем же все вот это надо и почему бы не использовать везде Flat подход? Посмотрите на сводную таблицу плюсов и минусов
EAV:
+ Более гибкая система чем Flat
+ При добавлении нового атрибута нет необходимости реиндексировать данные
+ Практически не ограниченное количество атрибутов
+ Всегда доступны все атрибуты
+ Статик атрибуты (sku, created_at, updated_at) всегда присутствуют в выборке, даже если их не указывать специально
— Fatal error: Call to a member function getBackend() при выборке/фильтрации по не существующему атрибуту
— Производительность

Flat:
+ Производительность
+ В выборку/фильтрацию могут быть применены только существующие атрибуты, которые добавлены во Flat таблицу
— Ограничение на размер строки (до 65,535 байт, т.е. 85 varchar 255) и количеству столбцов (InnoDB до 1000, некоторые до 4096)
— Используется только при работе с коллекциям (при загрузке всегда используется EAV)
— Результат отличается от выдачи запроса при EAV (отсутствуют статик атрибуты)
— После включения требуется реиндексация, в противном случае будут использованы EAV таблицы
— При добавлении нового атрибута необходимо реиндексировать Flat таблицы



Cache


Конечно каждый из вас может мне сказать, что зачем нам разбираться как ускорить запросы в БД и вообще как работают коллекции если кэш нас спасет и все будет закэшировано. Отвечу коротко – кэш вас не спасет. Ни 1 из кэшей представленных в Magento либо не кэширует коллекции автоматически либо не работает в ваших кастомных контроллерах и моделях, которые вы используете, скажем, при импорте данных или подсчете чего-то. Да и к тому же до того, как оно попадет в кэш, ведь надо это как-то туда положить и быстренько показать пользователю.

Типы кэшей в Magento 1.*:



  • Configuration – кэширует конфигурационные файлы
  • Layout – кэширует layout файлы
  • Block HTML output – кэширует phtml шаблоны. По умолчанию используется на фронтенде только в top menu и footer.
  • Translations – кэширует csv транслейт файлы
  • Collections data – кэширует коллекции, которые используют ->initCache(…) метод. По умолчанию кэширует только core_store, core_store_group, core_website коллекции при инициализации.
  • EAV types and attributes – должен кэшировать eav атрибуты, НО не кэширует. Используется в 1 методе, который никогда не вызывается начиная с Magneto CE 1.4
  • Web services cache – кэширует api.xml файлы
  • Page Cache (FPC) – кэширует весь HTML, кэширует только CMS, Category, Product страницы. Игнорируется если протокол https, гет параметр ?no_cache=1, куки NO_CACHE
  • DDL Cache (Скрытый) – кэширует DESCRIBE вызовы к БД, используется в операциях записи

…и ни 1 не кэширует коллекции автоматически.


Правильная работа с коллекциями


Для того, чтобы показать более наглядно почему что-то надо делать иначе чем многие привыкли, я решил привести некоторые тесты производительности разных подходов. Начнем пожалуй с тестового стенда. Для тестирования я использовал:

Тестовый стенд:
OS X 10.10
3.1 GHz Intel Core i5 (4 cores)
8GB

Magento конфигурация:
Magento EE 1.14.0
MySQL 5.5.38
PHP 5.6.2

Контент:
3 Categories
2000 Products
2000 CMS pages

Процесс:
Для тестов был создан экстеншен с 1 контроллером и 1 экшеном, каждый тест проводился 5 раз, потом считалось среднее время. Все результаты указаны в секундах.

class Test_Test_IndexController extends Mage_Core_Controller_Front_Action
{
    public function indexAction()
    {
        $temp = array();
        $start = microtime(true);
        Init values

        Loop start
            $temp[] = $product->getSku();
        Loop end
        Or
        Some code snippet

        $stop = microtime(true);
        echo $stop - $start;
    }
}

Псевдо код

Тесты


  1. EAV/Flat с перезагрузкой моделей и без
  2. Кэширование коллекций
  3. Правильное использование count() и getSize()
  4. Правильное использование getFirstItem и setPage(1,1)

EAV/Flat с перезагрузкой моделей и без


Цикл по коллекции. С load (перезагрузка) моделей внутри цикла:

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect(...);
foreach ($collection as $product) {
    $product = $product->load($product->getId());
    $temp[] = $product->getSku();
}

Цикл по коллекции. Без load моделей внутри:

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect(...);
foreach ($collection as $product) {
    $temp[] = $product->getSku();
}

3 вида выборки данных:

  1. addAttributeToSelect(‘*’); // все атрибуты
  2. addAttributeToSelect(‘sku’); // 1 статик атрибут
  3. addAttributeToSelect(‘name’); // 1 стандартный атрибут

Результаты


Как вы видимо заметили, время без перезагрузки моделей В РАЗЫ меньше, чем когда вы перезагружаете модельки. Так же время еще меньше, когда Flat таблицы включены (т.е. нет лишних джойнов и юнионов) и мы выбираем только необходимые атрибуты.

В 1ом случае мы выполняем загрузку с кучей джойнов… а потом делаем это снова, но для модельки и так 2000 раз.

2ой раз мы делаем это для статик атрибута (он находится в той же табличке, что и сам продукт) и Magento не надо делать джойны. Поэтому время меньше.

3ий раз Magento нужно приджойнить еще табличку где хранится этот атрибут.

С Flat таблицами все аналогично, а в 2ух случаях вcе идентично – это потому что оба атрибута находятся в 1 таблице, отсюда и время идентичное.

Думаю цифры говорят сами за себя.


Кэширование коллекций


Без кэша:

$collection = Mage::getModel('catalog/product')->getCollection()
                                               ->addAttributeToSelect('*');

Используя метод initCache:

$collection = Mage::getModel('catalog/product')->getCollection()
                                               ->addAttributeToSelect('*')
                                               ->initCache(Mage::app()->getCache(),'our_data',array('SOME_TAGS'));

Кастомная реализация кэширования:

$cache = Mage::app()->getCache();
$collection = $cache->load('our_data');
if(!collection) {
	$collection = Mage::getModel('collection/product')->getCollection()->addAttributeToSelect('*')->getItems();
	$cache->save(serialize($collection),'our_data',array(Mage_Core_Model_Resource_Db_Collection_Abstract::CACHE_TAG));
} else {
	$collection = unserialize($collection);
}

Рассмотрим выборку без использования кэша, с использованием метода, который нам предлагает Magento и с костылем, который я нигде не видел… сам сваял, основанный на методах модельки кэша. Обратите внимание, что для всех тестов после составления запроса я производил загрузку данных и преобразование коллекции к массиву объектов.

Результаты


Без кэша собственно ничего удивительного…все как обычно.

А вот используя маджентовский кэш я лично удивился, когда увидел, что время стало больше. А про EAV кэширование вообще глупой затеей, потому что EAV коллекция грузит сначала сущности из таблицы продуктов (именно вот это и кэшируется), а потом отдельным запросом выбирает значения атрибутов и заполняет объекты. Во Flat там все из 1 таблицы гонится. Но тем не менее время больше уходится на работу с кэшем чем с БД (тестировал я причем как с файловой системой, так и с redis – отличия 4ая цифра после запятой…т.е. на 2к сущностях ее нет). Суть InitCache метода заключается в том, что он сначала соберет все данные в коллекцию сам (пагинация, фильтры, events и так далее), создаст хеш из sql запроса и его будет искать в кэше, а если там что-то есть, то он это ансерелизует, а потом происходит запуск всех events и последующих методов. Это самая медленная процедура во всем процессе, именно вот тут выходит что кэш медленнее чем простой запрос в БД. Но зато не шлет запрос в БД… что не так и страшно уже.

Отдельно стоит пример с кэшем, написанным мной на коленке, там мы кэшируем конечный результат коллекции, причем минуя все events и дозагрузку атрибутов. Это работает для EAV и для Flat коллекций.

Правильное использование count() и getSize()


getSize()

$size = Mage::getModel('catalog/product')->getCollection()
                                         ->addAttributeToSelect('*')
                                         ->getSize();

count()

$size = Mage::getModel('catalog/product')->getCollection()
                                         ->addAttributeToSelect('*')
                                         ->count();

Результаты


Разница методов заключается в том, что count() производит загрузку всех объектов коллекции, а потом обычным пхпшным count’ом подсчитывает количество объектов и возвращает нам число. getSize же не производит загрузку коллекции, а генерирует еще 1 запрос в БД, где нет лимитов, ордеров и списка выбираемых атрибутов, есть только COUNT(*).

Пример использования обоих методов такой:

Если вам надо знать, есть ли вообще значения в БД или сколько их – используйте getSize, если же вам в любом случае коллекция нужна загруженная, или уже загрузилась то используйте count() – он вернет вам число элементов, загруженных в коллекцию.

Правильное использование getFirstItem и setPage(1,1)


getFirstItem()

$product = Mage::getModel('catalog/product')->getCollection()
                                            ->getFirstItem();

setPage(1,1)

$product = Mage::getModel('catalog/product')->getCollection()
                                            ->setPage(1,1)
                                            ->getFirstItem();

load()

$product = Mage::getModel('catalog/product')->load(22);

Результаты


Проблема getFirstItem в том, что он загружает всю коллекцию целиком, а потом просто в foreach возвращает первый элемент, а если его нет то возвращает пустой объект.

setPage (он же $this->setCurPage($pageNum)->setPageSize($pageSize)) же ограничивает выборку ровно 1 записью, что как вы видите значительно ускоряет загрузку результата.

Даже load быстрее getFirstItem, но обратите внимание, что load медленнее оказался чем выборка из коллекции 1 элемента. Это связано с тем, что load всегда работает с EAV таблицами.



Выводы


Подводя итог всему выше написанному, хочу посоветовать всем людям, работающим с Magento:

  • Никогда не вызывайте повторно load метод у объектов, полученных из коллекции
  • Загружайте только необходимые атрибуты
  • Если применимо к проекту, используйте Flat таблицы
  • Используйте count для подсчета результатов загруженной коллекции и getSize для получения числа всех записей
  • Не используйте getFirstItem метод без setPage(1,1) или аналогичных методов

Комментарии (20)


  1. Kudja
    20.04.2016 19:03

    Для тех у кого есть опыт вещи уже знакомые, но я всегда за материалы по magento


  1. my_lord
    21.04.2016 00:18

    Эх, донести бы до разработчиков тем для Magento.
    Также хочу добавить что для коллекций товаров есть атрибуты, которые загружаются по-умолчанию, если не указаны в addAttributeToSelect.
    Такие атрибуты берутся из настроек атрибутов каталога в админке и файле etc/config.xml модуля по пути config/frontend/product/collection/attributes.
    Разработчики тем порой об этом не знают и им проще сделать выбор коллекции и заново загрузить модель, у которой будут все ее атрибуты.


  1. maghamed
    22.04.2016 17:43

    >но описанное так же подходит и для Magento 2.*

    А почему вы это написали? Особенно то, что касается кода (кода коллекций в ваших примерах), я бы сказал, что не валидно по отношению к М2 чуть более чем полностью.

    Ну и ваши рекомендации, они фактически не про Мадженто, они про Базу Данных.
    Select * from table
    работает медленней чем
    select field from table

    Поэтому не доставайте ненужные поля.
    Считайте каунт не загружая записей из БД.


    1. Oxidant
      22.04.2016 19:03

      Если вы собрались копировать код 1 в 1, как делают многие со стек оверфлоу, то конечно ничего не будет работать.


      1. maghamed
        22.04.2016 23:25

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


        1. Oxidant
          22.04.2016 23:46

          Пожалуйста предоставьте пример того, где вы используете и как вы используете сервис лаер. В м2 точно так же есть коллекции, которые работают апсолютно так же как и в м1. Отличие разве что в получении объекта коллекции.


          1. maghamed
            23.04.2016 09:08

            Ну так с этого нужно начинать, по-видимому вы не знакомы с концептом сервис леера. Поэтому я предлагаю вам почитать соответствующие статьи
            Magento 2 Service Layer
            Service Contracts in Magento
            Magento 2 Service Contracts Patterns
            Github What is the goal of the service layer in Magento 2

            То, что в М2 есть коллекция — вас должно интересовать в самую последнюю очередь, если вы вдруг не найдете Сервис Контракта, удовлетворяющего ваши нужды. Во всех же обычных случаях вы будете использовать репозиторий сущности (EntityRepositoryInterface) и его метод getList(SearchCriteria $searchCriteria).
            Потому что в противном случае вы зависите не на интерфейс, а на реализацию и соответственно fragility такого кода очень высока. С зависимостью на интерфейс, реализация может кастомизироваться и подменяться, но ваш код это не поломает.


            1. maghamed
              23.04.2016 09:21

              Поэтому если кратко, то в вашем коде вы используете сервис леер везде. И только если вы не находите в нем нужного метода, тогда используете коллекцию или модель напрямую.


            1. Oxidant
              23.04.2016 09:53

              Я попросил предоставить пример где вы его используете в реальном проекте и как вы его используете в реальном проекте. С учетом всех фильтраций и так далее.


              1. Oxidant
                23.04.2016 10:02

                Проблема, я думаю в том, что сервис лаер «хорошо» описан в документациях и используется в паре мест в самой мадженте, но там в нем есть смысл ибо эти объекты используются в апи. В большинстве ( если не во всех) сторонних экстеншенах, которые сейчас выпускаются для М2 используют коллекции. М2 команда сейчас проверяет все экстеншены перед публикацией. Не в даваясь в вопросы как это они их пропускают, мы возвращаемся к первой проблеме, что люди редко умеют правильно использовать коллекции и грузятся все что не попадя, как в м1 так и в м2.


                1. maghamed
                  23.04.2016 10:37

                  Сервис леер не плохо описан в документации, и если по нему есть открытые вопросы их всегда можно адресовать на Magento 2 Github или Magento stack exchange.
                  Также в самой системе он используется достаточно много, а те места где он не используется — можно считать техническим долгом, который постепенно будет рефакториться. Основная идея — модули могут «общаться» друг с другом только по средствам сервис контрактов.
                  Экстеншены, которые пишут партнеры используют сервис леер, и разработчики мадженто за этим следят.
                  Поэтому исходя из всего вышеперечисленного, Коллекции — это вынужденная необходимость, которую можно использовать, если нет необходимого сервис контракта. Но никак не рекомендованный механизм для разработчика.


                  1. flancer
                    24.04.2016 09:43

                    Для интереса залез в код. Вот интерфейс \Magento\Catalog\Api\ProductRepositoryInterface, в нем описан метод


                    /**
                     * Get product list
                     *
                     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
                     * @return \Magento\Catalog\Api\Data\ProductSearchResultsInterface
                     */
                    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); 

                    Заглядываем в реализацию этого метода \Magento\Catalog\Model\ProductRepository::getList:


                    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
                    {
                        /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */
                        $collection = $this->collectionFactory->create();
                        $this->extensionAttributesJoinProcessor->process($collection);
                    
                        foreach ($this->metadataService->getList($this->searchCriteriaBuilder->create())->getItems() as $metadata) {
                            $collection->addAttributeToSelect($metadata->getAttributeCode());
                        }
                        $collection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner');
                        $collection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner');
                    
                        //Add filters from root filter group to the collection
                        foreach ($searchCriteria->getFilterGroups() as $group) {
                            $this->addFilterGroupToCollection($group, $collection);
                        }
                        /** @var SortOrder $sortOrder */
                        foreach ((array)$searchCriteria->getSortOrders() as $sortOrder) {
                            $field = $sortOrder->getField();
                            $collection->addOrder(
                                $field,
                                ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC'
                            );
                        }
                        $collection->setCurPage($searchCriteria->getCurrentPage());
                        $collection->setPageSize($searchCriteria->getPageSize());
                        $collection->load();
                    
                        $searchResult = $this->searchResultsFactory->create();
                        $searchResult->setSearchCriteria($searchCriteria);
                        $searchResult->setItems($collection->getItems());
                        $searchResult->setTotalCount($collection->getSize());
                        return $searchResult;
                    }

                    Сразу же бросается в глаза, что, в отношении получения списка записей, "репозиторий" — это всего лишь обертка над той же старой доброй коллекцией со всеми ее достоинствами и недостатками (включая невозможность иметь композитный primary key, которая меня почему-то огорчает больше всего).


                    Если исполнение "технического долга" со стороны Magento 2 Team сводится к оборачиванию коллекций в код, имплементирующий некий общий EntityRepositoryInterface (которого, кстати, нет, хотя он просто обязан быть, если "разработчики мадженто за этим следят"), то все слова в отношении коллекций, сказанные коллегой Oxidant, верны также и для "обернутых коллекций". Если же в недрах Magento 2 есть "типовая" (или хотя бы "эталонная") имплементация "усредненного" интерфейса EntityRepositoryInterface (save, get, getById, delete, deleteById, getList) без использования коллекций внутри, то был бы весьма признателен, если бы коллега maghamed дал ссылку на эту имплементацию.


                    1. maghamed
                      24.04.2016 16:19
                      +1

                      Окей, давайте по порядку.
                      Репозиторий это не «всего лишь обертка над коллекцией», репозиторий разделяет уровень доменных сущностей от слоя дата маппинга. Фактически это реализация классического Фаулеровского паттерна http://martinfowler.com/eaaCatalog/repository.html
                      Можно сказать, что репозитории представляют из себя коллекцию сущностей (Агрегейшен Рутов http://martinfowler.com/bliki/DDD_Aggregate.html в понятии DDD), которая агностична к тому, где эти сущности сохраняются и как.
                      Все репозитории являются частью публичного АПИ, предполагается, что репозитории будут выполнять роль Entry Point-a для работы с доменными сущностями. Поэтому именно репозитории будут плагинизироваться больше всего.

                      Сейчас для имплементации ф-ии getList практически все (вероятно даже все) репозитории используют механизм коллекций для реализации механизма фильтрации.
                      Более того, когда вы будете реализовывать АПИ для своего модуля, в реализации репозитория для сущности вашего модуля вы будете использовать коллекцию тоже. Потому что пока в Мадженто нет другого механизма, он должен появиться с версии 2.1
                      Но главное тут то, что код бизнес логики у вас (также как и в коде Мадженто) должен зависеть на RepositoryInterface, а не на коллекцию. Использование коллекции — это деталь реализации, которая постепенно будет меняться. Поэтому вы сами, как разработчик собственного модуля заинтересованы в том, чтобы изолировать код, который впоследствии станет деприкейтед и не плодить зависимости на него.

                      Композитный primary key не нужен, та же Доктрина не рекомендует использовать композитный составной ключ.

                      > Если исполнение «технического долга» со стороны Magento 2 Team сводится к оборачиванию коллекций в код,

                      Идея сводиться к тому, что нужно ввести стабильные АПИ, которые будут использовать и расширять. И иметь возможность безболезненно менять реализацию, которая кроется за этими АПИ. В данном случае Коллекции — реализация, и будет меняться на EntityManager который появится в версии 2.1

                      >EntityRepositoryInterface (которого, кстати, нет, хотя он просто обязан быть, если «разработчики мадженто за этим следят»)

                      Вот этого не понял. Почему в Мадженто должен быть общий интерфейс на репозитории? Его не должно быть и его нет.

                      >то все слова в отношении коллекций, сказанные коллегой Oxidant, верны также и для «обернутых коллекций».

                      Тут важно понимать следующее, основной посыл Oxidant сводился к тому, что программисты с недостаточным объемом знаний Мадженто, используют коллекции неправильно, когда пишут бизнес логику. Мой посыл в том, что эти программисты не должны использовать коллекции вообще. Они должны использовать репозитории, валидное использование коллекций сейчас может быть в реализации репозитория для собственного модуля, в остальном использования коллекций стоит избегать.


                      1. flancer
                        24.04.2016 22:22

                        Спасибо за развернутый ответ, коллега. Насколько я понял, на данный момент альтернативы коллекциям в механизмах фильтрации нет, появится он с версии 2.1, там и посмотреть можно будет, как его использовать.


                        Что касается составного первичного ключа, то это чисто мои тараканы — я считаю что для описания предметной области хватает простого первичного ключа, а для описания отношений между объектами предметной области простого первичного ключа уже недостаточно. Если та же Доктрина считает по-другому — это ее право.


                        Общий интерфейс на репозитории? Ну, если API так важен для архитектуры Magento 2, если код бизнес-логики должен быть завязан на Repository Interface, если большинство таких интерфейсов описывают методы для базовой манипуляции сущностями, то с моей точки зрения вполне логично выделить типовой набор методов (save, get, getById, delete, deleteById, getList) и обозвать его как-то типа BaseRepositoryInrerface, наследуя от него все остальные репо-интерфейсы. По крайней мере это было бы отличным маркером для читающих код и нечитающих мануалы, что это связанное подмножество интерфейсов, имеющее весомое значение в архитектуре Magento 2. Плюс, это было удобным примером для объяснения новичкам, что собственно такое "репозиторий сущности (EntityRepositoryInterface) и его метод getList(SearchCriteria $searchCriteria)", не на словах, а в коде. Но разработчики Magento 2 считают по-другому, поэтому такого интерфейса нет.


                        1. maghamed
                          24.04.2016 22:57
                          +1

                          Да, альтернативы коллекциям для реализации фильтрации сейчас нет. Но это попрежнему не значит, что ими можно пользоваться в коде бизнес логики.


                          Относительно первичного ключа — суррогатный ключ решает проблемы отношений.


                          Теперь самое важное — общий интерфейс. Разработчики Magento в данном вопросе считают правильно. А именно — что общий интерфейс BaseRepositoryInrerface вводить не нужно, более того это не правильно.


                          Взять, например интерфейс категории:


                          namespace Magento\Catalog\Api;
                          
                          /**
                           * @api
                           */
                          interface CategoryRepositoryInterface
                          {
                              /**
                               * Create category service
                               *
                               * @param \Magento\Catalog\Api\Data\CategoryInterface $category
                               * @return \Magento\Catalog\Api\Data\CategoryInterface
                               * @throws \Magento\Framework\Exception\CouldNotSaveException
                               */
                              public function save(\Magento\Catalog\Api\Data\CategoryInterface $category);
                          
                              /**
                               * Get info about category by category id
                               *
                               * @param int $categoryId
                               * @param int $storeId
                               * @return \Magento\Catalog\Api\Data\CategoryInterface
                               * @throws \Magento\Framework\Exception\NoSuchEntityException
                               */
                              public function get($categoryId, $storeId = null);
                          
                              /**
                               * Delete category by identifier
                               *
                               * @param \Magento\Catalog\Api\Data\CategoryInterface $category category which will deleted
                               * @return bool Will returned True if deleted
                               * @throws \Magento\Framework\Exception\InputException
                               * @throws \Magento\Framework\Exception\StateException
                               * @throws \Magento\Framework\Exception\NoSuchEntityException
                               */
                              public function delete(\Magento\Catalog\Api\Data\CategoryInterface $category);
                          
                              ....
                          

                          Как видите каждый из методов в своем контракте завязан на дата интерфейс категории \Magento\Catalog\Api\Data\CategoryInterface, т.е. все методы: get, getList, save, delete завязаны на контракт специфичного дата интерфейса.


                          Если представить, что последовать вашему примеру и ввести BaseRepositoryInrerface, то он должен в своих контрактах использовать BaseEntityInterface. Маркерный интерфейс, который расширят все Дата интерфейсы в системе. Это антиконцептуально с точки зрения доменного дизайна. Более того — это исключительно маркерный интерфейс, т.к. у нас нет ни одного метода, который бы мы могли туда положить. Даже getId не можем, потому что у сущностей с натуральным первичным ключем (например, продукт) в роли ID выступает SKU, который типа string, а не int. Естественно у такой BaseEntityInterface не можем быть реализации. Это просто маркерный интерфейс.
                          Поэтому Magento программисты решили договориться, и использовать во всех *RepositoryInterface одинаковый набор методов с одинаковыми параметрами, но завязанными на определенный тип сущности, как в моем примере выше на CategoryInterface


                          1. flancer
                            25.04.2016 07:36

                            По поводу коллеций я уже понял, и я нигде не настаивал на том, что коллекции нужно использовать в коде бизнес-логики :)


                            Первичный ключ — любой суррогат заменяет оригинал не на все 100%. Я конкатенировал в коллекциях два атрибута в один первичный ключ — для выборки данных это отработало, но манипуляцию (сортировка, фильтрация) я уже копать не стал. Как говорится, каждому овощу свое место, а коллекции — не то место, где можно использовать составные первичные ключи.


                            Если следовать примеру и вводить BaseRepositoryInrerface, то он может, но не обязательно должен в своих контрактах использовать BaseEntityInterface. Мне моя религия позволяет не указывать типы аргументов и возвращаемого результата, описывая только имена контрактов и набор входных аргументов (кол-во, порядок и соглашение по их наименованию). Моя религия говорит, что договор через код, гораздо более сильный чем договор через документацию, и уж куда более сильный, чем договор на словах. А раз уж PHP позволяет специализировать тип аргумента в производных интерфейсах, то я бы этой возможностью и воспользовался. Я не настаиваю, что это правильно в рамках какой-то другой религии (DDD, например), я просто говорю, что я бы сделал именно так просто потому, что это делает разработку кода с моей точки зрения несколько легче. У разработчиков Magento 2 другая точка зрения, и они действуют по-другому. Я не призываю менять свои религии, если что. Я просто поделился своей точкой зрения.


              1. maghamed
                23.04.2016 10:26

                Я же выше написал — везде где только можно. Потому что это АПИ, и Мадженто гарантирует, что она не сломает их PATCH и MINOR релизах, для коллекций и моделей такой гарантии нет.
                А какие проблемы у вас возникают с фильтрацией, которые вы не можете решить с помощью SearchCriteriaBuilder?


                1. Oxidant
                  23.04.2016 14:14

                  Быстрый взгляд на код м2 позволяет сказать, что для доступа к данным product alert stock используются коллекция, для самой важной страницы категории (продукт лист) используется коллекция, страница search модуля в админке использует коллекцию… Поэтому я думаю мы с вами говорим сейчас о разном применении этих вещей.

                  Я выше просил вас предоставить пример реального использования сервис лаера, но давайте будем более детальными. Предоставьте пожалуйста пример создания кастомного модуля, со своей таблицей в бд и с реализацией сервиса для доступа к моделям для работы с этой бд с учетом различных фильтров (Search criteria). Это поможет нам говорить о более детально примере, а не об абстрактных вещах. Все ссылки выше предоставляют общую информацию и нигде нет примера реального использования этого для чего-то выходящего за рамки нативного функционала, кроме вырванных из контекса примеров и обещаний в будущем все переписать. То что в мадженте есть они и они используются для доступа к данным — это определенно так ( продукты, цмс,… ). Но они не заменяют коллекции повсевместно. По крайней мере на данный момент.

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


  1. maghamed
    22.04.2016 17:55

    Единственный совет специфичный для Мадженто — это включите Flat вместо Eav


    1. Oxidant
      22.04.2016 18:58

      Но почему то и этого не делают :)