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