工厂方法模式和单例模式在 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 类的子类来实现,通过单例模式,我们提供了访问唯一的具体搜索子类的全局访问接口。通过以上这些方法,我们实现了灵活的数据库对象的搜索、缓存功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容