2

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.

2

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/

  • 1
    Wow, thanks! I had already implemented something like your second suggestion, but the first one actually made it fly. Saving the stock items is now about 6 times faster than before, and my memory exhaustion issues are gone. :) Just one small correction that I had to make: <stock_item>Vendor_Module_Model_CatalogInventory_Resource_Stock_Item</stock_item>: It's actually "Resource Stock Item" and not just "Resource Item". – Louis B. May 19 '16 at 13:25
  • @LouisBataillard yep you're right I've fixed that. As I said there's always several ways of improving scripts, the one I mentionned are the most common – Raphael at Digital Pianism May 19 '16 at 13:27
  • I'm having trouble with implementing the module. The core directory is : /app/code/core/Mage/CatalogInventory/‌​Model/Resource/Stock so in the custom model it shoud be : /app/code/local/Mymodule/CatalogInventor‌​y/Model/Reso‌​urce/Stock. I'm confused with this Model/CatalogInventory/Resource/Stock – mockingbid Aug 25 '16 at 14:31
  • @mockingbid I tend to avoid using the same module name as a core Magento module. So it's usually Vendor_Module. On top of that when I rewrite existing classes I put them in a folder named after the original Magento module. In this case as I rewrote a CatalogInventory module class I put them under a CatalogInventory folder – Raphael at Digital Pianism Aug 25 '16 at 14:57
  • How would you define the model in this case, what do i put in <resourceModel></resourceModel> when i define the model in config.xml . I'm not familiar with this and i get Fatal error: Call to undefined method Mage_CatalogInventory_Model_Resource_Stock_Item::saveAttribute() – mockingbid Aug 25 '16 at 15:31
2

When I have been doing things like this the best use of memory management that I found was to clear the variables that are no longer in use. You are creating an instance of a product but not assigning this to anything, so your probably getting memory leak there.

I suggest trying something like:

$tProduct = Mage::getModel('cataloginventory/stock_item')->loadByProduct($product);
$tProduct->setData(...);
$tProduct->save();
$tProduct->clearInstance();
unset($tProduct);

This should clean up from memory what was used in the process of your update, and remove it from reference then reinitialise just one on each iteration.

  • Thanks, I didn't know about "clearInstance" before, seems very useful in this context. – Louis B. May 19 '16 at 13:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.