Magento开发系列之十 Varien数据集合

早期PHP开发者如果想要将一组相关的变量放在一起,只能通过数组,虽然和C语言的内存地址数组同名,PHP的数组更多的是一个对象加序列索引的数组行为组成的通用字库。在其它语言中都有多种数据结构,每种结构在存储、速度和语法上有不同的优势。PHP语言的逻辑是减去这些选择而仅使用一种有用的数据结构即可。从PHP 5开始通过提供内置类和接口来让开发者允许创建自己的数据结构。

$array = new ArrayObject();
class MyCollection extends ArrayObject{}
$collection = new MyCollection();
$collection[] = 'bar';

一些软件工程师肯定还不满足于此,因为没有获取到底层实现的细节,但已经获得包含特定功能的方法来创建类似数组的对象。也可以设定一定的安全规则来只允许某些类型的对象进行集合,Magento中包含一些这种集合,事实上每个遵循Magento接口的模型对象都会获得一个集合类型。要成为有效的Magento开发者,了解这些集合的工作机制也非常重要。本节我们就来看看Magento的集合,创建一个可以自由添加代码的控制器action。

第一步需要创建一些新的对象

$thing_1 = new Varien_Object();
$thing_1->setName('Richard');
$thing_1->setAge(24);

$thing_2 = new Varien_Object();
$thing_2->setName('Jane');
$thing_2->setAge(12);

$thing_3 = new Varien_Object();
$thing_3->setName('Spot');
$thing_3->setLastName('The Dog');
$thing_3->setAge(7);

Varien_Object类定义了所有Magento模型所继承的类,这面向对象的系统中这很常见,它可以保证可以轻易地为每个对象添加方法、功能而无需编辑每个类文件。每个继承Varien_Object的对象都会获得魔术方法getter和setter,这样就可以通过get和set来获取并设置数据。比如,

var_dump($thing_1->getName());

如果不知道要获取的属性名,可以将所有数据取出到一个数组中,如

var_dump($thing_3->getData());

上面的代码将返回下面这样的数组:

array
'name' => string 'Spot' (length=4)
'last_name' => string 'The Dog' (length=7)
'age' => int 7

你可能会注意到last_name这个属性,中间是由下划线来划分的,如果想要使用getter和setter魔术方法,需要采用驼峰法来进行命名:

$thing_1->setLastName('Smith');

在较新版本的Magento中,可以使用数组形式的中括号来取得属性值:

var_dump($thing_3["last_name"]);

这是PHP5中强大的功能,现在我们创建了一些对象,让我们一起来加入到集合中吧,要记得集合类似数组,但是是由PHP开发人员定义的:

$collection_of_things = new Varien_Data_Collection();
$collection_of_things
->addItem($thing_1)
->addItem($thing_2)
->addItem($thing_3);

Magento中的数据集合通常都继承自Varien_Data_Collection,所有Varien_Data_Collection中的方法都可以被调用,对于集合我们可以用foreach函数来进行遍历

foreach($collection_of_things as $thing)
{
    var_dump($thing->getData());
}

还可以通过以下方法取出集合中的第一项和最后一项:

var_dump($collection_of_things->getFirstItem());
var_dump($collection_of_things->getLastItem()->getData());

如果想要以XML形式获取集合中的数据,可以使用如下方法

var_dump( $collection_of_things->toXml() );

那如果只要取出指定的属性呢?

var_dump($collection_of_things->getColumnValues('name'));

Magento开发团队甚至还添加了过滤功能:

var_dump($collection_of_things->getItemsByColumnValue('name','Spot'));

为什么要特别地讲这些东西呢?因为所有的内置数据集合都继承自这个对象,也就是说可以对产品集合等进行同样的操作,比如:

public function testAction()
{
    $collection_of_products = Mage::getModel('catalog/product')->getCollection();
    var_dump($collection_of_products->getFirstItem()->getData());
}

Magento中大多数的模型对象都有一个getCollection方法,它可以实例化集合并返回所有系统中支持的对象类型。数据集合包含很多决定使用index还是缓存的复杂逻辑,还有针对EAV实体的一些逻辑。在同一个集合中连续多次调用同一方法可能会产生一些不可控的效果,因此下面所有的例子都是放在一个action方法中进行的,建议在独自测试时也这么做。另外var_dump对于对象和集合相当的有用,因为通常针对大规模递归的对象时,它也会很智能的显示出对象的结构。产品集合还有其它的一些Magento集合,还继承了Varien_Data_Collection_Db,这样就可以使用很有有用的方法,比如要查看集合的select语句:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	var_dump($collection_of_products->getSelect());
}

输出结果如下:

object(Varien_Db_Select)#86 (5) {
["_bind":protected]=> array(0) { }
["_adapter":protected]=> object(Magento_Db_Adapter_Pdo_Mysql)

看上去一头雾水,由于Magento使用了Zend数据抽象层,select也是对象,我们修改代码如下:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	// var_dump($collection_of_products->getSelect());
	var_dump((string) $collection_of_products->getSelect());
}

这次的输出结果可能是如下的这种简单SQL:

string(49) &quot;SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code>&quot;

也有可能是更为复杂的语句:

string 'SELECT <code>e</code>.*, <code>price_index</code>.<code>price</code>, <code>price_index</code>.<code>final_price</code>, IF(<code>price_index</code>.<code>tier_price</code>, LEAST(<code>price_index</code>.<code>min_price</code>, <code>price_index</code>.<code>tier_price</code>), <code>price_index</code>.<code>min_price</code>) AS <code>minimal_price</code>, <code>price_index</code>.<code>min_price</code>, <code>price_index</code>.<code>max_price</code>, <code>price_index</code>.<code>tier_price</code> FROM <code>catalog_product_entity</code> AS <code>e</code>
INNER JOIN <code>catalog_product_index_price</code> AS <code>price_index</code> ON price_index.entity_id = e.entity_id AND price_index.website_id = '1' AND price_index.customer_group_id = 0'

这种区别主要取决于所select的属性,以及前所提到的index和cache的问题。如果学习过前面的章陈明,就知道Magento的很多模型包括产品模型使用EAV系统,默认情况下EAV集合不会包含所有对象属性,可以通过使用addAttributeToSelect方法来添加所有属性:

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

也可以只添加一个属性

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

或者多个属性:

$collection_of_products = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('meta_title')->addAttributeToSelect('price');

很多刚接触Magento ORM系统的PHP开发者总是搞不清楚该在什么时候进行数据库调用。在书写SQL语句甚至是使用基础的ORM系统时,SQL调用都在实体化对象当时发生。

$model = new Customer();
//SQL Calls being made to Populate the Object
echo 'Done'; //execution continues

但在Magento中并不是这样,它使用了延时加载(Lazy Loading)的概念。简单地说,延时加载是指只有在客户端程序需要获取数据时才会执行SQL调用,也就是如果进行如下操作:

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

Magento并没有操作数据库,因此你可以在后面再添加属性:

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

这里完全不用担心Magento会在每次添加属性时都去进行数据库查询,这些查询只有到了需要获取集合中的数据时才会进行。

数据库集合中最重要的一个方法是addFieldToFilter,这就添加了WHERE语句,可以使用自己的SKU也可以安装测试数据来进行如下测试:

public function testAction(){
	$collection_of_products = Mage::getModel('catalog/product')->getCollection();
	$collection_of_products->addFieldToFilter('sku','n2610');
	echo "Our collection now has ". count($collection_of_products). ' item(s)';
	var_dump($collection_of_products->getFirstItem()->getData());
}

addFieldToFilter中的第一个参数就是我们想要过滤的属性,而第二个则是查询的值。本例中我们添加了sku作为过滤,值为n2610。第二个参数也可以用于指定想要过滤的类型,这个有些复杂,不过值得更深入地研究一下,所以默认情况下:

$collection_of_products->addFieldToFilter('sku','n2610'); 

相当于

WHERE sku = "n2610"

接下来运行一下如下语句:

public function testAction(){
	var_dump((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku','n2610')->getSelect());
}

将会得到如下结果:

string(77) &quot;SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (<code>e</code>.<code>sku</code> = 'n2610')&quot;

注意一旦我们使用了EAV属性,就会变得更为复杂,下面添加一个属性:

var_dump((string) Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*')->addFieldToFilter('meta_title','my title')->getSelect());

输出会得到下面这样复杂的SQL语句:

string(670) &quot;SELECT <code>e</code>.*, IF(at_meta_title.value_id &gt; 0, at_meta_title.value, at_meta_title_default.value) AS <code>meta_title</code> FROM <code>catalog_product_entity</code> AS <code>e</code> INNER JOIN <code>catalog_product_entity_varchar</code> AS <code>at_meta_title_default</code> ON (<code>at_meta_title_default</code>.<code>entity_id</code> = <code>e</code>.<code>entity_id</code>) AND (<code>at_meta_title_default</code>.<code>attribute_id</code> = '82') AND <code>at_meta_title_default</code>.<code>store_id</code> = 0 LEFT JOIN <code>catalog_product_entity_varchar</code> AS <code>at_meta_title</code> ON (<code>at_meta_title</code>.<code>entity_id</code> = <code>e</code>.<code>entity_id</code>) AND (<code>at_meta_title</code>.<code>attribute_id</code> = '82') AND (<code>at_meta_title</code>.<code>store_id</code> = 1) WHERE (IF(at_meta_title.value_id &gt; 0, at_meta_title.value, at_meta_title_default.value) = 'my title')&quot;

那如果我们不只想要在查询中使用等号呢?比如大于、小于、不等于等等。addFieldToFilter中的第二个参数可以解决这一问题,在这里可以不传入一个字符串,而是传入一个数组。比如我们进行如下修改:

public function testAction(){
	var_dump((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku',array('eq'=>'n2610'))->getSelect());
}

其中的过滤即为:

addFieldToFilter('sku',array('eq'=>'n2610'))

可以看出,第二个参数是一个数组,键为eq,表示equals(等于),而值为n2610,即为想要过滤的值。Magento中有很多半英文的过滤符号,和perl中的基本一致,下面是一些过滤方式及其对应的SQL语句:

array("eq"=>'n2610')
WHERE (e.sku = 'n2610')
 
array("neq"=>'n2610')
WHERE (e.sku != 'n2610')
 
array("like"=>'n2610')
WHERE (e.sku like 'n2610')
 
array("nlike"=>'n2610')
WHERE (e.sku not like 'n2610')
 
array("is"=>'n2610')
WHERE (e.sku is 'n2610')
 
array("in"=>array('n2610'))
WHERE (e.sku in ('n2610'))
 
array("nin"=>array('n2610'))
WHERE (e.sku not in ('n2610'))
 
array("notnull"=>'n2610')
WHERE (e.sku is NOT NULL)
 
array("null"=>'n2610')
WHERE (e.sku is NULL)
 
array("gt"=>'n2610')
WHERE (e.sku > 'n2610')
 
array("lt"=>'n2610')
WHERE (e.sku &lt; 'n2610')
 
array("gteq"=>'n2610')
WHERE (e.sku >= 'n2610')
 
array("moreq"=>'n2610') //a weird, second way to do greater than equal
WHERE (e.sku >= 'n2610')
 
array("lteq"=>'n2610')
WHERE (e.sku &lt;= 'n2610')
 
array("finset"=>array('n2610'))
WHERE (find_in_set('n2610',e.sku))
 
array('from'=>'10','to'=>'20')
WHERE e.sku >= '10' and e.sku &lt;= '20

基本上一看就能够明白,这里挑出几个特别的解释一下

in, nin

in和nin条件语句中可以传入一组值,这里值的部分也可以使用数组。

array("in"=>array('n2610','ABC123')
WHERE (e.sku in ('n2610','ABC123'))

notnull, null

NULL类型在SQL中非常特别,不能通过等号运算符来过滤,通常使用null或notnull即可进行过滤,后面传入的值会被忽略掉

array("notnull"=>'n2610')
WHERE (e.sku is NOT NULL)

from – to

这是另外一个各其它语句不一样的格式,它传入的不是只有一对键值的数组,而是拥有两对键值,一个用于from,另一个用于to。从键名就可以猜它是用于限定一个范围的:

public function testAction
{
        var_dump( (string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('price',array('from'=>'10','to'=>'20'))->getSelect());                      
}

将会生成如下的SQL语句:

WHERE (_table_price.value >= '10' and _table_price.value <= '20')

最后我们来看看布尔值操作符,我们很少会只过滤一个属性,Magento自然早已考虑到这一情况。可以通过调用多个addFiledToFilter生成AND查询

function testAction()
{
        echo( (string) Mage::getModel('catalog/product') ->getCollection()->addFieldToFilter('sku',array('like'=>'a%'))->addFieldToFilter('sku',array('like'=>'b%'))->getSelect());
}

将生成如下SQL语句:

SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (<code>e</code>.<code>sku</code> LIKE 'a%') AND (<code>e</code>.<code>sku</code> LIKE 'b%')

可以发现以上WHERE语句中有一个AND,当然,这条语句不会返回任何,因为sku不可能同时以a和b开头。那这种情况下我们应用使用OR语句,要这么做需要在addFieldToFilter的第二个参数处传一个过滤数组,通过定义变量来做为数组可能更为直观:

$filter_a = array('like'=>'a%');
$filter_b = array('like'=>'b%');

那么最终我们将使用如下方法:

public function testAction()
{
        $filter_a = array('like'=>'a%');
        $filter_b = array('like'=>'b%');
        echo((string) Mage::getModel('catalog/product')->getCollection()->addFieldToFilter('sku',array($filter_a,$filter_b))->getSelect());
}

这次我们将得到如下的SQL语句:

SELECT <code>e</code>.* FROM <code>catalog_product_entity</code> AS <code>e</code> WHERE (((<code>e</code>.<code>sku</code> LIKE 'a%') OR (<code>e</code>.<code>sku</code> LIKE 'b%')))