Magento EAV Loading and Saving

What makes something either a simple Model or an EAV Model in Magento is its Model Resource.

Whenever you’ve defined a custom model, resource model and collection, more than likely your resource model will extend the Mage_Core_Model_Resource_Db_Abstract class.

An EAV resource model extends the Mage_Eav_Model_Entity_Abstract class, rather than Mage_Core_Model_Resource_Db_Abstract.

Their collections also extend the Mage_Eav_Model_Entity_Collection class, rather than Mage_Core_Model_Resource_Db_Collection_Abstract.

It’s worth noting that all models still extend from the Mage_Core_Model_Abstract class.

Another difference between a non EAV model and an EAV model is the __construct method within the resource model class.

Whenever you define a resource model for an EAV model.

public function __construct()
{
    parent::__construct();
    $this->setType(Mage_Catalog_Model_Product::ENTITY)
         ->setConnection('catalog_read', 'catalog_write');
    $this->_productWebsiteTable  = $this->getTable('catalog/product_website');
    $this->_productCategoryTable = $this->getTable('catalog/category_product');
}

The key lines here are the setType() and setConnection() methods being used.

The setType() method sets the entity type to catalog_product as defined near the top of the Mage_Catalog_Model_Product class.

const ENTITY                 = 'catalog_product';

A non EAV resource model uses the _construct() method and not __construct().

protected function _construct()
{
    $this->_init('newsletter/subscriber', 'subscriber_id');
    $this->_subscriberLinkTable = $this->getTable('newsletter/queue_link');
    $this->_read = $this->_getReadAdapter();
    $this->_write = $this->_getWriteAdapter();
}

The key line here is the _init() method which is used when you want to add our own custom resource model.

Now that we’ve seen some of the differences between the two models, we can take a look at how EAV models are loaded and saved.

If we look at the catalog product entity as an example, we use the following line to load a product with an ID of 1.

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

So what happens next? The load() method of the Mage_Core_Model_Abstract class is called.

abstract class Mage_Core_Model_Abstract extends Varien_Object {
    ....
    public function load($id, $field=null)
    {
        $this->_beforeLoad($id, $field);
        $this->_getResource()->load($this, $id, $field);
        $this->_afterLoad();
        $this->setOrigData();
        $this->_hasDataChanges = false;
        return $this;
    }
    ....
}

The _beforeLoad() method simply adds the object, field and id into an array that can be used by looking onto a couple of events that are dispatched here: model_load_before and core_abstract_load_before.

protected function _beforeLoad($id, $field = null)
{
    $params = array('object' => $this, 'field' => $field, 'value'=> $id);
    Mage::dispatchEvent('model_load_before', $params);
    $params = array_merge($params, $this->_getEventData());
    Mage::dispatchEvent($this->_eventPrefix.'_load_before', $params);
    return $this;
}

We then look for the model’s resource model by using the _getResource()->load() method.

public function load($object, $entityId, $attributes = array())
{
    $this->_attributes = array();
    return parent::load($object, $entityId, $attributes);
}

This load() method from the parent class is called. This class extends the Mage_Eav_Model_Entity_Abstract class that contains the load() method.

public function load($object, $entityId, $attributes = array())
{
    Varien_Profiler::start('__EAV_LOAD_MODEL__');
    /**
     * Load object base row data
     */
    $select  = $this->_getLoadRowSelect($object, $entityId);
    $row     = $this->_getReadAdapter()->fetchRow($select);

    if (is_array($row)) {
        $object->addData($row);
    } else {
        $object->isObjectNew(true);
    }

    if (empty($attributes)) {
        $this->loadAllAttributes($object);
    } else {
        foreach ($attributes as $attrCode) {
            $this->getAttribute($attrCode);
        }
    }

    $this->_loadModelAttributes($object);

    $object->setOrigData();
    Varien_Profiler::start('__EAV_LOAD_MODEL_AFTER_LOAD__');

    $this->_afterLoad($object);
    Varien_Profiler::stop('__EAV_LOAD_MODEL_AFTER_LOAD__');

    Varien_Profiler::stop('__EAV_LOAD_MODEL__');
    return $this;
}

The interesting line to look at here is the $this->loadAllAttributes() line.

public function loadAllAttributes($object=null)
{
    $attributeCodes = Mage::getSingleton('eav/config')
        ->getEntityAttributeCodes($this->getEntityType(), $object);

    /**
     * Check and init default attributes
     */
    $defaultAttributes = $this->getDefaultAttributes();
    foreach ($defaultAttributes as $attributeCode) {
        $attributeIndex = array_search($attributeCode, $attributeCodes);
        if ($attributeIndex !== false) {
            $this->getAttribute($attributeCodes[$attributeIndex]);
            unset($attributeCodes[$attributeIndex]);
        } else {
            $this->addAttribute($this->_getDefaultAttribute($attributeCode));
        }
    }

    foreach ($attributeCodes as $code) {
        $this->getAttribute($code);
    }

    return $this;
}

A list of attribute codes are obtained and stored in the $attributeCodes variable.

Back to the load() method, the _loadModelAttributes() method returns us a row of data within the catalog_product_entity table.

protected function _loadModelAttributes($object)
{
    if (!$object->getId()) {
        return $this;
    }

    Varien_Profiler::start('__EAV_LOAD_MODEL_ATTRIBUTES__');

    $selects = array();
    foreach (array_keys($this->getAttributesByTable()) as $table) {
        $attribute = current($this->_attributesByTable[$table]);
        $eavType = $attribute->getBackendType();
        $select = $this->_getLoadAttributesSelect($object, $table);
        $selects[$eavType][] = $this->_addLoadAttributesSelectFields($select, $table, $eavType);
    }
    $selectGroups = Mage::getResourceHelper('eav')->getLoadAttributesSelectGroups($selects);
    foreach ($selectGroups as $selects) {
        if (!empty($selects)) {
            $select = $this->_prepareLoadSelect($selects);
            $values = $this->_getReadAdapter()->fetchAll($select);
            foreach ($values as $valueRow) {
                $this->_setAttributeValue($object, $valueRow);
            }
        }
    }

    Varien_Profiler::stop('__EAV_LOAD_MODEL_ATTRIBUTES__');

    return $this;
}

Returning back to the load() method of the Mage_Core_Model_Abstract class, the _afterLoad() simply dispatches another two events.

protected function _afterLoad()
{
    Mage::dispatchEvent('model_load_after', array('object'=>$this));
    Mage::dispatchEvent($this->_eventPrefix.'_load_after', $this->_getEventData());
    return $this;
}

Note that the load() method is different in non EAV models whose resource models extends the Mage_Core_Model_Resource_Db_Abstract class. We simply fetch the row from the database table.

public function load(Mage_Core_Model_Abstract $object, $value, $field = null)
{
    if (is_null($field)) {
        $field = $this->getIdFieldName();
    }

    $read = $this->_getReadAdapter();
    if ($read && !is_null($value)) {
        $select = $this->_getLoadSelect($field, $value, $object);
        $data = $read->fetchRow($select);

        if ($data) {
            $object->setData($data);
        }
    }

    $this->unserializeFields($object);
    $this->_afterLoad($object);

    return $this;
}

Moving onto saving data, let’s set our product’s name to something different for example and call the save() method like the example below.

$product = Mage::getModel('catalog/product')->load(1);
$product->setName("Some Product Name");
$product->save();

The save() method of the Mage_Core_Model_Abstract class is called.

public function save()
{
    /**
     * Direct deleted items to delete method
     */
    if ($this->isDeleted()) {
        return $this->delete();
    }
    if (!$this->_hasModelChanged()) {
        return $this;
    }
    $this->_getResource()->beginTransaction();
    $dataCommited = false;
    try {
        $this->_beforeSave();
        if ($this->_dataSaveAllowed) {
            $this->_getResource()->save($this);
            $this->_afterSave();
        }
        $this->_getResource()->addCommitCallback(array($this, 'afterCommitCallback'))
            ->commit();
        $this->_hasDataChanges = false;
        $dataCommited = true;
    } catch (Exception $e) {
        $this->_getResource()->rollBack();
        $this->_hasDataChanges = true;
        throw $e;
    }
    if ($dataCommited) {
        $this->_afterSaveCommit();
    }
    return $this;
}

Before we save the data, a couple of if statements are present to check whether the product should be deleted and whether or not the product model has actually changed.

Similar to the load() process, we encounter _beforeSave() and _afterSave() methods. The _beforeSave() looks like the following.

protected function _beforeSave()
{
    if (!$this->getId()) {
        $this->isObjectNew(true);
    }
    Mage::dispatchEvent('model_save_before', array('object'=>$this));
    Mage::dispatchEvent($this->_eventPrefix.'_save_before', $this->_getEventData());
    return $this;
}

A check to see if the ID exists for our product is made. If it doesn’t, we simply mark the object as new. We also have the model_save_before and core_abstract_save_before events dispatched.

The line:

$this->_getResource()->save($this);

Will firstly fetch the model’s resource model and then call the save() method. As we’re continuing with our product example, the resource model will be the same as when we were looking at the load() example; Mage_Catalog_Model_Resource_Product.

The Mage_Catalog_Model_Resource_Product extends the Mage_Eav_Model_Entity_Abstract class which contains the save() method.

public function save(Varien_Object $object)
{
    if ($object->isDeleted()) {
        return $this->delete($object);
    }

    if (!$this->isPartialSave()) {
        $this->loadAllAttributes($object);
    }

    if (!$object->getEntityTypeId()) {
        $object->setEntityTypeId($this->getTypeId());
    }

    $object->setParentId((int) $object->getParentId());

    $this->_beforeSave($object);
    $this->_processSaveData($this->_collectSaveData($object));
    $this->_afterSave($object);

    ....
}

The loadAllAttributes() method will load all of the attributes from the EAV tables of the catalog_product entity.

We then collect and process the save data that sit in between _beforeSave() and _afterSave() method of the EAV abstract class.

Returning back to Mage_Core_Model_Abstract, the _afterSave() method is run:

protected function _afterSave()
{
    $this->cleanModelCache();
    Mage::dispatchEvent('model_save_after', array('object'=>$this));
    Mage::dispatchEvent($this->_eventPrefix.'_save_after', $this->_getEventData());
    return $this;
}

A summary of the load and save process is as follows.

  • The load() method of Mage_Core_Model_Abstract is called.
  • _beforeLoad() method of Mage_Core_Model_Abstract is called that dispatches model_load_before and core_abstract_load_before events.
  • The load() method of the model’s resource model is called which delegates to the load() method of the Mage_Eav_Model_Entity_Abstract class.
  • _afterLoad() is called which dispatches model_load_after and core_abstract_load_after.
  • setOrigData() is then called.
  • The save() method of Mage_Core_Model_Abstract is called.
  • _beforeSave() method of Mage_Core_Model_Abstract is called that dispatches model_save_before and core_abstract_save_before events.
  • The save() method of Mage_Eav_Model_Entity_Abstract is called.
  • _beforeSave() of the model’s resource model is called.
  • processData() is then called.
  • _afterSave() of the model’s resource model is called.
  • _afterSave() method of Mage_Core_Model_Abstract is called that dispatches model_save_after and core_abstract_save_after events.
  • afterCommitCallback() is then called.

Note: This article is based on Magento Community/Open Source version 1.9.