How to improve
performance of product
attributes
Bartosz Gorski, community member for over a decade.
Disclaimer
The views and opinions expressed in this presentation are solely my own and do not represent the views,
opinions, or policies of my employer or any affiliated organization. This content is provided for informational
purposes only. Any actions taken based on the information presented are done at the recipient's own risk,
and I or my employer assume no responsibility or liability for any outcomes resulting from such actions.
Problem Statement
The more attributes we add to products, the worse the performance gets, since it has to load data from
multiple places.
This is less visible for “regular” (or technically speaking: “custom”) attributes, but becomes more of an issue
in case of “extension” attributes, especially ones that are complex entities loaded from their own tables.
How custom and extension attributes
work?
Custom attributes:
- Are added via admin panel or data patches
- Are usually loaded with the rest of product data whenever a product gets added
- Usually represent a single value or a set of values of a single “thing” about a product
Extension attributes:
- Can represent a simple thing like a custom attribute, but usually represents a whole different model,
like a Stock Item attached to a product
- Can contain custom and extension attributes of its own
- Are NOT loaded automatically with the rest of the product data, instead require a custom logic for
being attached, unless we specify a JOIN when defining the attribute
Examples of custom and extension
attributes
Custom attributes:
- energy efficiency (A++, A+, A, B etc.)
- legacy_id (ID from before a migration to Commerce)
Extension attributes:
- vendor (a separate object containing all the vendor data like name, address, custom notes etc.)
- shipping_package_info (a separate object containing all the info about how the product is shipped like
dimensions, material used for packing etc.)
- legacy_id (because yes, we can also use extension attributes for such simple data)
Custom attributes - how they are loaded
Loaded automatically with the rest of the product data when:
- Products are loaded via repository (in any way - get, getList etc.)
- Products are loaded using a resource model
Not loaded automatically when:
- Products are loaded using a collection
It’s worth to mention that REST products endpoint will load all attributes even if you selected only a subset.
Custom attributes - how they are loaded
https://m247.test/rest/default/V1/products/?searchCriteria[filter_groups][0][filters][0]
[field]=sku&searchCriteria[filter_groups][0][filters][0][value]=%dynamic%&searchCriteria[filter_groups][0]
[filters][0][condition_type]=like&searchCriteria[pageSize]=20&searchCriteria[currentPage]=7
and
https://m247.test/rest/default/V1/products/?searchCriteria[filter_groups][0][filters][0]
[field]=sku&searchCriteria[filter_groups][0][filters][0][value]=%dynamic%&searchCriteria[filter_groups][0]
[filters][0]
[condition_type]=like&searchCriteria[pageSize]=20&searchCriteria[currentPage]=7&fields=items[sku,name]
Will return different responses, but will not differ in execution time.
Extension attributes - how they are
loaded
There are two types - ones with the JOIN defined and ones with no JOIN:
<extension_attributes for="Magento\Catalog\Api\Data\ProductInterface">
<attribute code="stock_item" type="Magento\CatalogInventory\Api\Data\StockItemInterface">
<join reference_table="cataloginventory_stock_item" reference_field="product_id" join_on_field="entity_id">
<field>qty</field>
</join>
</attribute>
</extension_attributes>
<extension_attributes for="Magento\Catalog\Api\Data\ProductInterface">
<attribute code="vendor" type="Magento\Framework\DataObject"/>
</extension_attributes>
The first one gets added as an SQL join only when getList() method from a repository is used.
The second one requires custom logic like Read Handler and Save Handler, or completely custom plugins.
Extension attributes - handlers
<type name="Magento\Framework\EntityManager\Operation\ExtensionPool">
<arguments>
<argument name="extensionActions" xsi:type="array">
<item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="array">
<item name="create" xsi:type="array">
<item name="create_bundle_options" xsi:type="string">Magento\Bundle\Model\Product\
SaveHandler</item>
</item>
<item name="update" xsi:type="array">
<item name="update_bundle_options" xsi:type="string">Magento\Bundle\Model\Product\
SaveHandler</item>
</item>
<item name="read" xsi:type="array">
<item name="read_bundle_options" xsi:type="string">Magento\Bundle\Model\Product\
ReadHandler</item>
</item>
</item>
</argument>
</arguments>
</type>
This works after the product is already loaded, and not at all when it’s loaded in a collection.
Extension attributes - plugins
Sometimes extension attributes are loaded and saved using plugins:
public function beforeLoad(\Magento\Sales\Model\ResourceModel\Order\Collection $subject)
public function afterLoad(
\Magento\Sales\Model\ResourceModel\Order $subject,
\Magento\Sales\Model\ResourceModel\Order $result,
\Magento\Framework\Model\AbstractModel $order
)
public function afterGetList(\Magento\Sales\Model\OrderRepository $subject, Collection $result)
public function afterGetExtensionAttributes(
CategoryInterface $entity,
CategoryExtensionInterface $extension = null
)
Beware of anything that works “after” as it will most likely introduce another db request.
Let’s start with the right choice
First of all, decide if you need a custom attribute or an extension attribute.
What to consider:
- is the attribute complex or simple?
- is it going to be used in every use case or at least in majority of cases that require a product to be
loaded?
- will the attribute be manipulated, or is only used for display purposes?
What if an attribute is needed only
conditionally or not always?
What you can do is to implement lazy loading and load some extension attributes only when you
really need them.
Here’s how - Step #1
Add product ID or SKU as an extension attribute along with the extension attribute you really intended to add:
<?xml version="1.0" encoding="utf-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.x
sd">
<extension_attributes for="Magento\Catalog\Api\Data\ProductInterface">
<attribute code="product_id" type="int"/>
<attribute code="my_extension_attribute" type="Vendor\Module\Api\Data\
MyDataInterface"/>
</extension_attributes>
</config>
Here’s how - Step #2
Create a plugin for $product->getExtensionAttributes() (the ProductInterface
interface)
<type name="Magento\Catalog\Api\Data\ProductInterface">
<plugin name="Vendor\Module\Plugin\MyProductPlugin"
type="Vendor\Module\Plugin\MyProductPlugin"/>
</type>
With the following logic:
public function afterGetExtensionAttributes(
\Magento\Catalog\Api\Data\ProductInterface $subject,
\Magento\Catalog\Api\Data\ProductExtensionInterface $result
): \Magento\Catalog\Api\Data\ProductExtensionInterface {
$result->setProductId((int)$subject->getId());
return $result;
}
Here’s how - Step #3
Create a plugin for the object containing product extension attributes:
<type name="Magento\Catalog\Api\Data\ProductExtensionInterface">
<plugin name="Vendor\Module\Plugin\MyProductExtensionPlugin"
type="Vendor\Module\Plugin\MyProductExtensionPlugin"/>
</type>
In there, add the logic for loading the required data (let’s assume the extension name is my_extension_attribute as defined in step #1):
public function afterGetMyExtensionAttribute(ProductExtensionInterface $subject, ?array $result
): ?array {
if ($result) {
return $result; // In case this got executed already
}
if (!$subject->getProductId()) {
return null; // In case this is executed on a ProductExtensionInterface not associated with any product
}
$myExtensionAttribute = $this->someNewFunctionLoadingTheData($subject->getProductId());
if (empty($myExtensionAttribute)) {
return null;
}
$subject->setMyExtensionAttribute($myExtensionAttribute);
return $myExtensionAttribute;
}
Pros and cons
Pros:
- It will not load the attribute in any executions that don’t actually need them
- Since there’s no overhead before calling $product->getExtensionAttributes()->getMyAttribute(), it’s still possible
to create more fine-tuned optimizations that work for multiple products at once (collection plugin, getList
plugin, GraphQL bulk resolver etc.) depending on exact execution paths in the project
Cons:
- In those executions that actually need those attributes to be there, loading them this way is more time- and
resource-consuming than methods working before products get loaded from the database since a separate
SELECT is required
Or better yet…
If you believe you need an extension attribute just for a specific use case, think if you need it to be an
extension attribute at all.
Usually it’s sufficient to separately load another object instead and just glue it together with the product data
before that data is returned via REST, GraphQL or some other method, without the overhead of that data
being an extension attribute at all.
Questions?
Happy to answer if there are any.
Thank you!
Feel free to get in touch if you have any questions later.
[email protected]
https://www.linkedin.com/in/gorskibartosz/