工厂方法模式和单例模式在 Laravel 框架中 ORM 搜索功能中的应用

Laravel 框架中 ORM 搜索结果缓存的实现

标签: 设计模式 工厂方法模式 单例模式 Laravel PHP


在 Gof 总结的 24 种设计模式中,用来分离类的创建与调用的工厂模式和单例模式的应用非常广泛 ,今天我们就来看一下这些模式在 Laravel 框架的 ORM 搜索结果缓存功能中的应用。

ORM 模式介绍

在使用 Laravel 框架或者其他框架的时候,ORM 的搜索功能是很重要的一块。我们知道,ORM 是一种关系模型映射,它将数据库中的表和编程语言中的类,表的字段和类的属性,表中的记录和类的实例对应起来,记录的增加和删除对应类对象的创建与删除,记录的修改对应对象属性的修改,而记录的查找则通过 ORM 模型提供的对数据库的查找操作的方法来实现。ORM 模型在本质上还是框架中数据库的操作模块的进一步封装。

问题描述

在我们使用框架中的 ORM 模型进行开发的时候,有时候可能需要对 ORM 模型进行进一步的扩展,比如数据库中的 product 表对应的 Product 模型,我们可能需要在上面扩展业务层和产品相关的功能。有时候可能需要对搜索功能进行进一步的优化,比如对搜索结果添加缓存功能。由于 ORM 模型的实现本身就具有复杂性,我们很难在 ORM 模型的基础上修改代码添加缓存功能,因此我们考虑将 ORM 模型的搜索功能进行抽象,创建独立的搜索类来实现这些功能。

工厂方法模式

Gof 总结的设计模式中,对工厂方法模式的描述如下:

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到子类。

单例模式

接下来我们来看一下单例模式的描述:

保证一个类仅有一个实例,并提供一个访问它的全局访问点

问题分析

我们面对的主要问题是对数据库搜索结果的缓存问题。我们可以创建一个抽象类来封装对数据库的查询、查询结果缓存等操作。在这个抽象类中,封装一个对数据库操作对象的属性,由其代理具体的数据库操作。同时创建一个抽象的工厂方法,让子类确定具体实例化哪一个 ROM 或者用户扩展的业务类实例。
因为我们搜索类属于工具类,原则上不应该管理有关上下文的数据信息,应该保证一个具体的类仅有一个具体的实例,因此我们用单例模式管理具体搜索类的实例,并提供访问具体唯一搜索类的全局访问接口。

代码实现

接下来我们先来看一下抽象的搜索类的实现

abstract class Search{
    /*
    * 搜索抽象类,管理项目中 ORM 实例的搜索、缓存等操作
    */
    
    //数据库操作对象的类实例
    protected $db;
    //要搜索的数据表
    protected $table;
    //要搜索的数据表的主键
    protected $primaryKey;
    //是否对搜索结果进行缓存
    protected $cache = true;
    
    //针对某一次具体的搜索行为是否要进行缓存
    protected $realCache;
    //存储实例化好的具体搜索类的实例
    protected static $_instance = [];

    protected function __construct()
    {
        $this->dbReset();
    }

    public function __call($name, $arguments)
    {
        //使用魔术方法,将具体的数据库操作的方法调用代理到 $this->db 对象上
        if(method_exists($this->db, $name)){
            call_user_func_array([$this->db, $name], $arguments);
        }
        return $this;
    }

    /**
     * @return Search
     */
    public static function Instance()
    {
        // 单例模式的访问接口,通过访问此方法,返回由静态属性 $_instance 管理的具体的搜索对象实例
        //返回的是调用此方法的类对象的实例
        $calledClass = get_called_class();
        if(!isset(self::$_instance[$calledClass])){
            self::$_instance[$calledClass] = new $calledClass();
        }else{
            self::$_instance[$calledClass]->dbReset();
        }
        return self::$_instance[$calledClass];
    }

    public function dbReset(){
        //重置 $this->db 以及其他上下文管理属性,以防以前的搜索条件对此次搜索产生影响
        unset($this->db);
        $this->db = DB::table($this->table)->select($this->primaryKey);
        $this->realCache = $this->cache;
    }

    public function cache($cache){
        /*
        *设计这个方法和 $this->realCache 属性的目的是有时候在项目正式环境中,可能需要对搜索结果进行缓存,但在调试的时候需要关掉缓存调试。通过设置 $this->realCache 为 false 可以关闭此次搜索的缓存。
        */
        $this->realCache = $cache;
        return $this;
    }

    public function page($page, $pageRow){
        //对分页操作进行封装
        $this->db->skip(($page - 1) * $pageRow)->limit($pageRow);
        return $this;
    }

    protected function cacheRemember($cacheId, Closure $callback){
        //对回调返回的数据进行缓存操作
        if(!$this->realCache){
            Cache::tags(['search', $this->table])->forget($cacheId);
        }

        if(Cache::tags(['search', $this->table])->has($cacheId)){
            return Cache::tags(['search', $this->table])->get($cacheId);
        }else{
            $data = $callback();
            if($this->cache){
                Cache::tags(['search', $this->table])->put($cacheId, $data, Constant::CACHE_TIME);
            }
            return $data;
        }
    }

    public function getIds(){
        //返回搜索的主键的集合,并进行缓存操作
        $cacheId = 'search-get-ids-'.md5($this->db->toSql().json_encode($this->db->getBindings()));

        $ids = $this->cacheRemember($cacheId, function(){
            $primaryKey = $this->primaryKey;
            if(strpos($this->primaryKey, '.') !== false){
                list(,$primaryKey) = explode('.', $this->primaryKey);
            }
            return $this->db->select($this->primaryKey)->lists($primaryKey);
        });
        return $ids;
    }

    public function count(){
        //返回搜索结果的数目,并进行缓存操作
        $cacheId = 'search-count-'.md5($this->db->toSql().json_encode($this->db->getBindings()));
        return $this->cacheRemember($cacheId, function(){
            return $this->db->distinct()->count();
        });
    }

    public function sum($column){
        //返回并缓存搜索结果某一列的和
        $cacheId = 'search-sum-'.md5($this->db->toSql().json_encode($this->db->getBindings()));
        return $this->cacheRemember($cacheId, function()use($column){
            return $this->db->sum($column);
        });
    }

    public function avg($column){
        //返回并缓存搜索结果某一列平均值
        $cacheId = 'search-avg-'.md5($this->db->toSql().json_encode($this->db->getBindings()));
        return $this->cacheRemember($cacheId, function()use($column){
            return $this->db->average($column);
        });
    }

    public function getByIds($ids){
        //根据主键返回模型实例集合
        $items = [];
        if(!empty($ids)){
            foreach($ids as $id){
                $item = $this->find($id);
                if($item){
                    $items[] = $item;
                }
            }
        }
        //可以对搜索结果用 Collection 对象进行封装
        return new Collection($items);
    }

    public function get(){
        //返回根据搜索结果得到的模型实例集合
        $result = $this->getByIds($this->getIds());
        $this->dbReset();
        return $result;
    }

    public function first(){
        //返回搜索到的第一个实例
        $this->limit(1);
        $ids = $this->getIds();
        $this->dbReset();
        return !empty($ids) ? $this->find($ids[0]) : null;
    }

    /**
     * @param $id
     * @return mixed
     * 抽象的工厂方法,将创建具体的 ORM 实例和对实例进行缓存的工作交给子类来实现。
     */
    abstract public function find($id);
}

至此,我们已经实现了抽象类 Search,它的子类将实现针对具体 ORM 的搜索工具类。接下来我们来看一下搜索 product 表对应 ROM 对象实例的搜索字类如何实现。

class ProductSearch extends Search
{
    /*
    * ProductSearch 类,提供针对 Product 表的对象搜索功能
    */
    protected $table = 'products as p';
    protected $primaryKey = 'p.pid';
    
    public function find($id)
    {
        // 返回以 $id 为主键的 Product 类的实例
        return Product::find($id);
    }
}

至此我们就实现了 ProductSearch 类的功能,我们就可以通过其提供的方法方便地进行 Product 类对象的搜索。比如通过调用 ProductSearch::Instance()->where('price','>',100)->get() 返回所有价格大于100的产品对象集合。

总结

为了实现项目中不同数据库对象的搜索功能,我们对数据库搜索功能进行抽象得到 Search 搜索类,其中数据库查询的功能由 Search 类的数据库操作属性来实现。并且通过工厂方法模式,我们将具体数据库对象的查询和实例化延迟到 Search 类的子类来实现,通过单例模式,我们提供了访问唯一的具体搜索子类的全局访问接口。通过以上这些方法,我们实现了灵活的数据库对象的搜索、缓存功能。

你可能感兴趣的:(工厂方法模式和单例模式在 Laravel 框架中 ORM 搜索功能中的应用)