一、概念
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)的程序设计。可以将延迟加载操作置于一个单独的方面,这样能独立改变延迟加载策略,领域开发者也不必处理延迟加载的问题了。
无论你是否在领域类中显式地添加延迟加载代码,延迟加载都是一个好习惯。除了类型安全之外,使用集合对象而非数组的好处是你可以使用延迟加载。