04月04, 2017

在 CodeIgniter 3 中高效使用 Model 类及其事务处理(1)

CodeIgniter 3 中,要使用数据库模型,通过继承【数据模型超类】CI_Model 就可以很方便的达到目的了。继承之后,当引用了不存在的属性时,则通过访问【数据模型超类】的魔术方法 __get 找到【控制器超类】 CI_Controller,【控制器超类】则把任务托管到【加载器超类】CI_Loader,调用【加载器超类】初始化方法 initialize,根据自动加载配置中的配置项完成整个运行环境的初始化。

//数据模型超类
class CI_Model
{
    ......
    /**
     * __get magic
     *
     * Allows models to access CI's loaded classes using the same
     * syntax as controllers.
     *
     * @param string $key
     */
    public function __get($key)
    {
        // Debugging note:
        // If you're here because you're getting an error message
        // saying 'Undefined Property: system/core/Model.php', it's
        // most likely a typo in your model code.
        return get_instance()->$key;
    }
}
//CI核心函数库
function &get_instance()
{
    return CI_Controller::get_instance();
}

//控制器超类
class CI_Controller
{

    /**
     * Reference to the CI singleton
     *
     * @var    object
     */
    private static $instance;

    /**
     * Class constructor
     *
     * @return    void
     */
    public function __construct()
    {
        self::$instance =& $this;

        // Assign all the class objects that were instantiated by the
        // bootstrap file (CodeIgniter.php) to local class variables
        // so that CI can run as one big super object.
        foreach (is_loaded() as $var => $class) {
            $this->$var =& load_class($class);
        }

        $this->load =& load_class('Loader', 'core');
        $this->load->initialize();
        log_message('info', 'Controller Class Initialized');
    }

    /**
     * Get the CI singleton
     *
     * @static
     * @return object
     */
    public static function &get_instance()
    {
        return self::$instance;
    }
}
//加载器超类
class CI_Loader
{
    ......
    public function initialize()
    {
        $this->_ci_autoloader();
    }
    ......
}

提出问题

在我看来,上面的描述实际不仅限于数据模型了,实际概括了CI运行环境初始化的主线,之所以强调数据模型,目的在于我要解决使用该模型时的不便之处(个人观点)。

  • 问题一:代码不简洁,到处都是 $this->load->model('Xxx_model'); $this->Xxx_model->xxxFunc(); 这样的代码
  • 问题二:事务处理使用不方便解耦合,若多数据模型之间存在原子操作时,更是繁琐

通过继承 CI_Model,我们实现一个或多个模型类,如下:

class Axxx_model extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
        $this->db = $this->load->database('master', true);
        $this->_db = $this->load->database('default', true);
    }

    public function axxxAdd($table, $data)
    {
        return $this->db->insert($table, $data);
    }

    public function axxxUpdate($table, $data)
    {
        return $this->db->update($table, $data);
    }

    public function axxxSelect()
    {
        $sql = $this->_db->get_compiled_select('channel_main');
        return $this->_db->query($sql);
    }
}

class Bxxx_model extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
        $this->db = $this->load->database('master', true);
        $this->_db = $this->load->database('default', true);
    }

    public function bxxxAdd($table, $data)
    {
        return $this->db->insert($table, $data);
    }

    public function bxxxSelect()
    {
        $sql = $this->_db->get_compiled_select('channel_main');
        return $this->_db->query($sql);
    }
}

假设我们没有对以上数据模型使用自动加载配置,那么在控制器中使用【加载器】手动导入数据模型类:

class Test extends CI_Controller
{
    public function __construct()
    {
        parent::__construct();
    }

    public function funcA()
    {
        $this->load->model('Axxx_model');
        $this->Axxx_model->axxxAdd('tableA', ['key'=>'value',......]);
        $this->Axxx_model->axxxUpdate('tableA', ['key'=>'value',......]);
    }
    public function funcB()
    {
        $this->load->model('Bxxx_model');
        $this->Bxxx_model->bxxxAdd('tableB', ['key'=>'value',......]);
    }
}

Axxx_model 中的 axxxAddaxxxUpdate 要做事务处理,把两个方法挪到一块,然后用CI的提供的事务吗,这样肯定不好的,本身实际应用中就有很多操作放到不同的方法中,达到解耦合的目的。之前我是通过克隆一个实例来做的,如下:

class Axxx_model extends CI_Model
{
    ......

    //Axxx_model中增加一个方法
    public function dbHandler()
    {
        return (clone $this->db);
    }
}

class Test extends CI_Controller
{
    ......

    //funcC走事务
    public function funcC()
    {
        $this->load->model('Axxx_model');
        $dbHandler = $this->Axxx_model->dbHandler();

        //开启事务
        $dbHandler->trans_begin();
        $this->Axxx_model->axxxAdd('tableA', ['key'=>'value',......]);
        $this->Axxx_model->axxxUpdate('tableA', ['key'=>'value',......]);
        if ($dbHandler->trans_status() === false) {
            $dbHandler->trans_rollback();
            //do something
        } else {
            $dbHandler->trans_commit();
            //do something
        }
    }
}

这样做也仅仅是可以处理单个模型中的事务问题了,如果原子操作分布在多个数据模型类中,那该怎么做事务处理呢?

因为每次在模型中通过 $this->db = $this->load->database('master', true); 得到的数据库连接都是不一样的,那么多个模型间做原子操作就显得更麻烦了。针对这一问题,我在后面的静态化实现中,也做了单例模式的实现(通过区分数据库配置名)。

解决问题

解决上面提到的问题,我的实现方式也参考了之前在 Laravel 框架中应用的DB类,如下:

//文件名:models/Mdb.php
<?php
/**
 * Mdb 自动加载的静态Model类
 * Autoload:
 *          //配置文件(config/autoload.php)中增加对该类的自动加载
 *          $autoload['model'] = array('Mdb');
 * @author xlang(xlangersir@gmail.com)
 */
class Mdb extends CI_Model
{
    private static $load; //静态引用CI_loader
    private static $curclass; //静态引用当前类
    private static $dbcontainer = array(); //数据库对象容器(区分配置名而不是调用名)
    private static $dbconf = array( //数据库调用配置:调用名 => 配置名
        'db' => 'master',
        '_db' => 'default'
    );

    public function __construct()
    {
        parent::__construct();
        self::$load =& $this->load;
        self::$curclass =& $this;
    }

    /**
     * 调用model
     * @param string $modelName 标准CI定义中的model类名称
     * @return mixed
     */
    public static function model($modelName)
    {
        self::$load->model($modelName);
        return self::$curclass->{$modelName};
    }

    /**
     * 事务执行
     * @param closure $closure 执行事务块
     */
    public static function transaction($closure)
    {
        $db = self::initDB('db');
        $db->trans_begin();
        try {
            call_user_func($closure);
            if ($db->trans_status() === false) {
                throw new \Exception('Transaction exception!');
            }
            $db->trans_commit();
        } catch (\Exception $e) {
            $db->trans_rollback();
            throw new \Exception($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * 获取指定配置的数据库对象
     * @param string $dbCallName 调用名
     * @return mixed
     */
    protected static function initDB($dbCallName)
    {
        $dbConfName = self::$dbconf[$dbCallName];
        if (!isset(self::$dbcontainer[$dbConfName]) || !is_a(self::$dbcontainer[$dbConfName], 'CI_DB')) {
            self::$dbcontainer[$dbConfName] = self::$load->database($dbConfName, true);
        }
        return self::$dbcontainer[$dbConfName];
    }

    public function __get($key)
    {
        if (array_key_exists($key,  self::$dbconf)) {
            return self::initDB($key);
        } else {
            return parent::__get($key);
        }
    }
}

然后,通过配置来自动加载 Mdb ,其他的数据模型类继承它 ,改造前面的两个model类,如下:

class Axxx_model extends Mdb
{
    public function __construct()
    {
        parent::__construct();
        //这都不需要了,调用到时自动获得实例
        //$this->db = $this->load->database('master', true);
        //$this->_db = $this->load->database('default', true);
    }

    public function axxxAdd($table, $data)
    {
        return $this->db->insert($table, $data);
    }

    public function axxxUpdate($table, $data)
    {
        return $this->db->update($table, $data);
    }

    ......
}

class Bxxx_model extends Mdb
{
    public function __construct()
    {
        parent::__construct();
    }

    public function bxxxAdd($table, $data)
    {
        return $this->db->insert($table, $data);
    }

    ......
}

控制器的逻辑应用,如下:

class Test extends CI_Controller
{
    ......

    public function funcA()
    {
        //调用数据模型的方法
        Mdb::model('Axxx_model')->axxxAdd('tableA', ['key'=>'value',......]);
        Mdb::model('Axxx_model')->axxxUpdate('tableA', ['key'=>'value',......]);
    }

    public function funcB()
    {
        //自动处理事务,原子操作的事务块放到闭包中
        Mdb::transaction(function () {
            //模型Axxx_model的操作
            Mdb::model('Axxx_model')->axxxAdd('tableA', ['key'=>'value',......]);
            Mdb::model('Axxx_model')->axxxUpdate('tableA', ['key'=>'value',......]);
            //模型Bxxx_model的操作
            Mdb::model('Bxxx_model')->bxxxAdd('tableB', ['key'=>'value',......]);

            //跑出异常或中断执行都自动导致回滚,否则自动提交
            //throw new Exception('hello world');
            //die('hello world');
        });
    }
}

另外,即便是继承了这个类,原有的实现和调用方式照样有效。

本文链接:https://xlange.com/post/static-call-model-for-codeigniter

-- EOF --

Comments

?