Magento 1.9 – Avoid Memory Exhaustion When Updating Product Stock

magento-1.9memoryperformancestock

I need to process a product collection and update the stock quantity and status for each product.

Currently I'm doing this:

foreach($products AS $product) {
    $quantity = $productQuantities[$product->getId()]; // A helper array that I've built before
    Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}

This works nicely, but I reach memory exhaustion after about 5k products, and I need to update more than 20k products.

Is there any Magento-internal way of avoiding this?

I do realize I could write directly to the DB, but I'd rather avoid this and go with a Magento-internal function.

Best Answer

Avoid saving entire objects

One of the biggest problem in your case is that you save the entire stock item every time.

It's not performance friendly.

For some models, Magento provides a saveAttribute method which can save you a lot of resources, however it does not exist for the cataloginventory/stock_item model.

So I reckon you should implement it via a custom module:

In your config.xml, you first need to override the resource model:

<models>
    <cataloginventory_resource>
        <rewrite>
            <stock_item>Vendor_Module_Model_CatalogInventory_Resource_Stock_Item</stock_item>
        </rewrite>
    </cataloginventory_resource>
</models>

Then in your Model/CatalogInventory/Resource/Stock/Item.php file:

<?php

class Vendor_Module_Model_CatalogInventory_Resource_Stock_Item extends Mage_CatalogInventory_Model_Resource_Stock_Item
{
    /**
     * Save attribute to model
     *
     * @param $model - the model
     * @param $attributes - array of attributes to get and save from model
     * @return $this
     * @throws Exception
     */
    public function saveAttribute($model, $attributes)
    {
        try {
            $adapter = $this->_getWriteAdapter();
            $adapter->beginTransaction();
            $condition = $this->_getWriteAdapter()->quoteInto($this->getIdFieldName() . '=?', $model->getId());
            $data      = array();
            foreach ($attributes as $attribute) {
                $value = $model->getData($attribute);
                if (isset($value)) {
                    $data[$attribute] = $value;
                }
            }
            if (!empty($data)) {
                $this->_getWriteAdapter()->update($this->getMainTable(), $data, $condition);
            }
            $adapter->commit();
        } catch (Exception $e) {
            $adapter->rollBack();
            throw $e;
        }
        return $this;
    }
}

Now in your code, replace:

Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();

With:

$stockItem = Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity);
$stockItem->getResource()->saveAttribute($stockItem,array('is_in_stock','qty'));

Avoid loading entire objects

Another issue in your case, is that you load entire objects.

Same as the load, it's pretty bad in terms of performance.

As you're dealing with 20k products, I'm not sure how it will work but maybe using collections will save you some time.

So instead of :

foreach($products AS $product) {
    $quantity = $productQuantities[$product->getId()]; // A helper array that I've built before
    Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}

Maybe you should try:

$stockItems = Mage::getResourceModel('cataloginventory/stock_item_collection')->addFieldToSelect('product_id')->addFieldToFilter('product_id',array('in'=>$productIds));
foreach($stockItems as $stockItem)
{
    $quantity = $productQuantities[$stockItem->getProductId()];
    $stockItem->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}

Where $productIds is an array containing all your product ids.

Avoid looping a collection

I don't know what your $products variable is but if it is a collection, I suggest you should avoid looping on it.

Magento provides a great iterator walk method with callback method to handle massive collection.

So instead of:

foreach($products AS $product) {
    $quantity = $productQuantities[$product->getId()]; // A helper array that I've built before
    Mage::getModel('cataloginventory/stock_item')->loadByProduct($product)->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}

You should try:

Mage::getSingleton('core/resource_iterator')->walk($products ->getSelect(), array(array($this, 'updateStock')));

Then declare the following method in your class:

public function updateStock($args)
{
    $quantity = $this->productQuantities[$args['row']['entity_id']];
    Mage::getModel('cataloginventory/stock_item')->loadByProduct($args['row']['entity_id'])->setData('is_in_stock', $quantity > 0 ? 1 : 0)->setQty($quantity)->save();
}

Note that you will have to make your productQuantities array accessible globally in your class to be able to do so.

More information about the walk iterator: http://inchoo.net/magento/working-with-large-magento-collections/