对象关系行为模式之延迟加载
一、概念
Lazy Load:一个对象,它虽然不包含所需要的所有数据,但是知道怎么获取这些数据。
延迟加载貌似很简单,就是在数据需要时再从数据库获取,减少数据库的消耗。但这其中还是有不少技巧的。
二、实现延迟加载
实现Lazy Load主要有四种方法:延迟初始化、虚代理、值保持器和重影。
(1)延迟初始化(Lazy initialization)
1.1 概念
这个是最简单的方法。意思就是每次访问属性域都要先检查该域是否为空,如果为空,再获取这个域的值。这样必须做到所有对该域的访问,即使来自类的内部,都要通过获取方法来实现。
1.2 代码实现
class Supplier{
private $products;
public function getProducts(){
if($products == null)
$products = $Product->findForSupplier();
return $products;
}
}
1.3 使用时机
只有在域需要另外的数据库访问时才考虑使用延迟加载。
需要额外的调用,并且当使用主对象时所调用的数据没有用到的时候。
最适用于活动记录、表数据入口和行数据入口。
(2)虚代理(virtual proxy)
2.1 概念
实质为一对象,不包含任何东西,只有当它的一个方法被调用时,它才从数据库加载恰当的对象。
说简单点说是一个对象的代理对象,初始化时不加载对象,只有当代理对象被调用方法时才真正去加载。
2.2 代码实现
/**
* 虚代理,只有在被访问成员时才调用闭包函数生成目标对象。
*/
class VirtualProxy
{
private $holder = null;
private $loader = null;
/**
* @param Closure $loader 生成被代理对象的闭包函数
*/
public function __construct(Closure $loader)
{
$this->loader = $loader;
}
/**
* 代理成员方法的调用
*
* @param string $method
* @param array $arguments
* @throws BadMethodCallException
* @return mixed
*/
public function __call($method, array $arguments = null)
{
$this->check();
if (!method_exists($this->holder, $method)) {
throw new BadMethodCallException();
}
return call_user_func_array(
array(&$this->holder, $method),
$arguments);
}
/**
* 代理成员属性的读取
*
* @param string $property
* @throws ErrorException
* @return mixed
*/
public function __get($property)
{
$this->check();
if (!isset($this->holder->$property)) {
throw new ErrorException();
}
return $this->holder->$property;
}
/**
* 代理成员属性的赋值
*
* @param string $property
* @param mixed $value
*/
public function __set($property, $value)
{
$this->check();
$this->holder->$property = $value;
}
/**
* 检查是否已经存在被代理对象,不存在则生成。
*/
private function check()
{
if (null == $this->holder) {
$loader = $this->loader;
$this->holder = $loader();
}
}
}
// 测试
$v = new VirtualProxy(function(){
echo 'Now, Loading', "\n";
$a = new ArrayObject(range(1,100));
$a->abc = 'a';
// 实际使用中,这里调用的是 DataMapper 的 findXXX 方法
// 返回的是领域对象集合
return $a;
});
// 代理对象直接当作原对象访问
// 而此时构造方法传入的 callback 函数才被调用
// 从而实现加载对象操作的延迟
echo $v->abc . $v->offsetGet(50);
(3)值保持器(value holder)
3.1 概念
一个用来包装某个其他对象的对象,要想获取基对象,可以访问值保持器得到它的值,但是只有第一次访问值保持器时它真正从数据库读取数据。
(4)重影(ghost)
4.1 概念
部分状态下的真实对象,当从数据库加载对象的时候,它只包含其ID。当每次要访问某个域时,它就会加载其完全状态。把重影看成一个对象,它的每个域都是被一下子延迟初始化的,或者把它看成一个虚代理,对象本身就是它的虚代理。
4.2 代码实现
//继承要加载的对象
class DeferredEventCollection extends EventCollection {
private $stmt;
private $valueArray;
private $run=false;//标识当前加载状态
//构造方法,不真正获取数据,只包含其$valueArray(ID)
function __construct( Mapper $mapper, \PDOStatement $stmt_handle,array $valueArray ) {
parent::__construct( null, $mapper );
$this->stmt = $stmt_handle;
$this->valueArray = $valueArray;
}
//加载完全状态
function notifyAccess() {
if ( ! $this->run ) {
$this->stmt->execute( $this->valueArray );
$this->raw = $this->stmt->fetchAll();
$this->total = count( $this->raw );
}
$this->run=true;
}
}
三、延迟加载的风险
1、继承常常会给延迟加载带来问题。如果要用重影,需要知道要创建什么类型的重影,如果没有正确加载数据,往往就很难说了。虚代理在静态数据类型语言中也会遇到同样的问题。
2、延迟加载容易导致产生超出需要的数据库访问。书中别名“波动加载”。如用延迟加载来填充一个集合,然后每次只访问其中一个元素。这样就会每访问一个对象就访问一次数据库,而不是一次把所需对象全部读出来。这种加载方式会严重影响系统的性能。当然,可以不使用延迟加载的集合,而使类集合本身成为延迟加载,加载类集合时一次加载全部内容。
四、小结
延迟加载很适合于面向方面(AOP)的程序设计。可以将延迟加载操作置于一个单独的方面,这样能独立改变延迟加载策略,领域开发者也不必处理延迟加载的问题了。
无论你是否在领域类中显式地添加延迟加载代码,延迟加载都是一个好习惯。除了类型安全之外,使用集合对象而非数组的好处是你可以使用延迟加载。