thinkphp3.2源码----获取并保存模型对象与数据库连接实例

摘自文档:

在ThinkPHP中基础的模型类就是 Think\Model 类,该类完成了基本的CURD、ActiveRecord模式、连贯
操作和统计查询,一些高级特性被封装到另外的模型扩展中。
基础模型类的设计非常灵活,甚至可以无需进行任何模型定义,就可以进行相关数据表的ORM和CURD操
作,只有在需要封装单独的业务逻辑的时候,模型类才是必须被定义的。

使用模型类我们可以对相关的数据表进行CURD操作,实例化模型类的方法是我们熟悉的M或者D方法,使用模型类之前,我们来看看thinkphp是然后让模型类去对数据表进行CURD操作的。

  • 使用M方法实例化模型
    因为D方法要先去查找某个具体的模型类,所有我们从M方法中去研究tp是如何获取数据库连接实例的。
    我们写下M(‘user’)运行,因为我的数据库中没有这张表,最后tp会抛出异常,在以前tp错误异常处理中也介绍过tp处理异常的方法,通过tp返回的回溯追踪我们来看看M方法的执行过程:
    thinkphp3.2源码----获取并保存模型对象与数据库连接实例_第1张图片
    通过上图我们可以看出,省去中间的一些细节操作,M的操作可以看成一下四步:

    thinkphp3.2源码----获取并保存模型对象与数据库连接实例_第2张图片

    • Think\Model->__construct
      源码(Thinkphp/Library/Think/Model.class.php):
 /**
     * 架构函数
     * 取得DB类的实例对象 字段检查
     * @access public
     * @param string $name 模型名称
     * @param string $tablePrefix 表前缀
     * @param mixed $connection 数据库连接信息
     */
    public function __construct($name = '', $tablePrefix = '', $connection = '')
    {
        // 模型初始化
        $this->_initialize();
        // 获取模型名称
        if (!empty($name)) {
            if (strpos($name, '.')) {
                // 支持 数据库名.模型名的 定义
                list($this->dbNamedbName, $this->name) = explode('.', $name);
            } else {
                $this->name = $name;
            }
        } elseif (empty($this->name)) {
            $this->name = $this->getModelName();
        }
        // 设置表前缀
        if (is_null($tablePrefix)) {
            // 前缀为Null表示没有前缀
            $this->tablePrefix = '';
        } elseif ('' != $tablePrefix) {
            $this->tablePrefix = $tablePrefix;
        } elseif (!isset($this->tablePrefix)) {
            $this->tablePrefix = C('DB_PREFIX');
        }

        // 数据库初始化操作
        // 获取数据库操作对象
        // 当前模型有独立的数据库连接信息
        $this->db(0, empty($this->connection) ? $connection : $this->connection, true);
    }

(这个构造方法里的 _initialize方法先放一下,在以后的博客中再和大家一起学习,这里就先按照注释来,它是起模型初始化的作用;)
这个构造方法主要的作用就是完善当前Model的一些属性,包括模型名称,表前缀,数据库连接信息等,准备完毕后最后执行了一个db()方法,通过这个db方法来获取数据库连接
db方法:

/**
     * 切换当前的数据库连接
     * @access public
     * @param integer $linkNum  连接序号
     * @param mixed $config  数据库连接信息
     * @param boolean $force 强制重新连接
     * @return Model
     */
    public function db($linkNum = '', $config = '', $force = false)
    {
        if ('' === $linkNum && $this->db) {
            return $this->db;
        }

        if (!isset($this->_db[$linkNum]) || $force) {
            // 创建一个新的实例
            if (!empty($config) && is_string($config) && false === strpos($config, '/')) {
                // 支持读取配置参数
                $config = C($config);
            }
            $this->_db[$linkNum] = Db::getInstance($config);
        } elseif (null === $config) {
            $this->_db[$linkNum]->close(); // 关闭数据库连接
            unset($this->_db[$linkNum]);
            return;
        }

        // 切换数据库连接
        $this->db = $this->_db[$linkNum];
        $this->_after_db();
        // 字段检测
        if (!empty($this->name) && $this->autoCheckFields) {
            $this->_checkTableInfo();
        }

        return $this;
    }

Think\Model的构造方法中是这样调用db方法的:
$this->db(0, empty($this->connection) ? $connection : $this->connection, true);
我们直接通过最后一个参数’true’就可以知道,Think\Model的构造方法中调用db方法是想创建一个新的数据库连接实例,所有我们直接看这一段:

 if (!isset($this->_db[$linkNum]) || $force) {
            // 创建一个新的实例
            if (!empty($config) && is_string($config) && false === strpos($config, '/')) {
                // 支持读取配置参数

                $config = C($config);
            }
            $this->_db[$linkNum] = Db::getInstance($config);
        }

这个流程中首先判断你是否传递了连接数据库的字符串参数,如果有就通过C函数去分解,并组成一个$config数组(具体分解组装操作看C函数的源码),然后调用Db::getInstance($config) 获取连接实例,并赋值给Model的数据连接对象池。
getInstance($config):

/**
     * 取得数据库类实例
     * @static
     * @access public
     * @param mixed $config 连接配置
     * @return Object 返回数据库驱动类
     */
    public static function getInstance($config = array())
    {
        $md5 = md5(serialize($config));
        if (!isset(self::$instance[$md5])) {
            // 解析连接参数 支持数组和字符串
            $options = self::parseConfig($config);
            // 兼容mysqli
            if ('mysqli' == $options['type']) {
                $options['type'] = 'mysql';
            }
            // 如果采用lite方式 仅支持原生SQL 包括query和execute方法
            $class = !empty($options['lite']) ? 'Think\Db\Lite' : 'Think\\Db\\Driver\\' . ucwords(strtolower($options['type']));
            if (class_exists($class)) {
                self::$instance[$md5] = new $class($options);
            } else {
                // 类没有定义
                E(L('_NO_DB_DRIVER_') . ': ' . $class);
            }
        }
        self::$_instance = self::$instance[$md5];
        return self::$_instance;
    }

getInstance方法中,首先处理传递过来的连接数据库参数,在$options = self::parseConfig($config); 一句中的parseConfig方法里可以看到,如果我们什么也没有传递过来,就读取我们的数据库配置,parseConfig:

 /**
     * 数据库连接参数解析
     * @static
     * @access private
     * @param mixed $config
     * @return array
     */
    private static function parseConfig($config)
    {
        if (!empty($config)) {
            if (is_string($config)) {
                return self::parseDsn($config);
            }
            $config = array_change_key_case($config);
            $config = array(
                'type'        => $config['db_type'],
                'username'    => $config['db_user'],
                'password'    => $config['db_pwd'],
                'hostname'    => $config['db_host'],
                'hostport'    => $config['db_port'],
                'database'    => $config['db_name'],
                'dsn'         => isset($config['db_dsn']) ? $config['db_dsn'] : null,
                'params'      => isset($config['db_params']) ? $config['db_params'] : null,
                'charset'     => isset($config['db_charset']) ? $config['db_charset'] : 'utf8',
                'deploy'      => isset($config['db_deploy_type']) ? $config['db_deploy_type'] : 0,
                'rw_separate' => isset($config['db_rw_separate']) ? $config['db_rw_separate'] : false,
                'master_num'  => isset($config['db_master_num']) ? $config['db_master_num'] : 1,
                'slave_no'    => isset($config['db_slave_no']) ? $config['db_slave_no'] : '',
                'debug'       => isset($config['db_debug']) ? $config['db_debug'] : APP_DEBUG,
                'lite'        => isset($config['db_lite']) ? $config['db_lite'] : false,
            );
        } else {
            $config = array(
                'type'        => C('DB_TYPE'),
                'username'    => C('DB_USER'),
                'password'    => C('DB_PWD'),
                'hostname'    => C('DB_HOST'),
                'hostport'    => C('DB_PORT'),
                'database'    => C('DB_NAME'),
                'dsn'         => C('DB_DSN'),
                'params'      => C('DB_PARAMS'),
                'charset'     => C('DB_CHARSET'),
                'deploy'      => C('DB_DEPLOY_TYPE'),
                'rw_separate' => C('DB_RW_SEPARATE'),
                'master_num'  => C('DB_MASTER_NUM'),
                'slave_no'    => C('DB_SLAVE_NO'),
                'debug'       => C('DB_DEBUG', null, APP_DEBUG),
                'lite'        => C('DB_LITE'),
            );
        }
        return $config;
    }

连接信息处理完后,就终于到了实例化数据库驱动的操作上了:

 // 如果采用lite方式 仅支持原生SQL 包括query和execute方法
            $class = !empty($options['lite']) ? 'Think\Db\Lite' : 'Think\\Db\\Driver\\' . ucwords(strtolower($options['type']));
            if (class_exists($class)) {
                self::$instance[$md5] = new $class($options);
            } else {
                // 类没有定义
                E(L('_NO_DB_DRIVER_') . ': ' . $class);
            }

在这里通过读取不同的配置信息,来决定调用哪一个数据库驱动,比如我的是Mysql数据库驱动,打印出这个$class 就是 “Think\Db\Driver\Mysql”,tp就调用Think\Db\Driver\Mysql.class.php这个Mysql驱动,这里就以Mysql驱动类为例吧:

// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st 
// +----------------------------------------------------------------------

namespace Think\Db\Driver;

use Think\Db\Driver;

/**
 * mysql数据库驱动
 */
class Mysql extends Driver
{

.......................

Mysql这个驱动类中并没有构造方法,但是他继承自Driver基类,先执行基类的构造方法:

Driver基类的构造方法( Think\Db\Driver.class.php):

 /**
     * 架构函数 读取数据库配置信息
     * @access public
     * @param array $config 数据库配置数组
     */
    public function __construct($config = '')
    {
        if (!empty($config)) {
            $this->config = array_merge($this->config, $config);
            if (is_array($this->config['params'])) {
                $this->options = $this->config['params'] + $this->options;
            }
        }
    }

配置完毕,现在Mysql这个类以及他的基类Driver都配置好了,最后一层一层的回到最开始的M函数:

/**
 * 实例化一个没有模型文件的Model
 * @param string $name Model名称 支持指定基础模型 例如 MongoModel:User
 * @param string $tablePrefix 表前缀
 * @param mixed $connection 数据库连接信息
 * @return Think\Model
 */
function M($name = '', $tablePrefix = '', $connection = '')
{
    static $_model = array();
    if (strpos($name, ':')) {
        list($class, $name) = explode(':', $name);
    } else {
        $class = 'Think\\Model';
    }
    $guid = (is_array($connection) ? implode('', $connection) : $connection) . $tablePrefix . $name . '_' . $class;

    if (!isset($_model[$guid])) {
        $_model[$guid] = new $class($name, $tablePrefix, $connection);
    }

    return $_model[$guid];
}

可以看到M函数返回的$_model[$guid] 就是我们前面解读的tp那一系列操作返回的数据库连接实例,以我现在配置的是Mysql驱动为例子,这个$_model[$guid] 是包含了Mysql.class.php以及Driver.class.php两个类的对象,而实例化这些数据库驱动又是通过实例化Model.class.php这个Model实现的,因此我们通过M函数就得到了三个实例对象:Model,Driver,Mysql,通过返回的这些对象就可以对数据库进行CURD操作了,我们平时用的所有关于数据库的操作都能在这个三个类中找到。
上述只是第一次使用M方法,当我们第二次或者第N次使用M方法时,tp又是如何保存连接对象和已经实例化的某一个模型对象呢?
首先我们来说保存已经实例化的模型对象:
在M中声明了一个静态属性$_model,当有相同的连接参数传入时,比如都是M(‘user’),
$guid = (is_array($connection) ? implode('', $connection) : $connection) . $tablePrefix . $name . '_' . $class;
在这里$guid这个变量就相当于记录的‘user’这个名字,后面再判断:
if (!isset($_model[$guid])) { 如果不存在‘$guid’所记录的名字才去实例化新的模型,所有说不管你用多少次M(‘user’) tp都只实例化了一次,极大的减少开销。
最后是怎么将保存连接对象:
如果是不同的模型,无论前面的代码怎么执行,最后要到Model类中的db方法(只有实例化模型类都要执行类的构造方法),执行db方法又回到了开始的Db类中去,Think\Db 下面的 Db.class.php这个类用一个比较时髦的词语来说就是门面,通过他决定你要走什么“门”,而在这个类中也有一个静态属性记录了你已经走过的“门牌号”,
Db.class.php:

// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st 
// +----------------------------------------------------------------------

namespace Think;

/**
 * ThinkPHP 数据库中间层实现类
 */
class Db
{

    private static $instance  = array(); //  数据库连接实例
    private static $_instance = null; //  当前数据库连接实例
    .........

在取得数据库类实例的静态方法getInstance是这样控制的:

 if (!isset(self::$instance[$md5])) {
            // 解析连接参数 支持数组和字符串
            $options = self::parseConfig($config);
            // 兼容mysqli
            if ('mysqli' == $options['type']) {
                $options['type'] = 'mysql';
            }
            // 如果采用lite方式 仅支持原生SQL 包括query和execute方法
            $class = !empty($options['lite']) ? 'Think\Db\Lite' : 'Think\\Db\\Driver\\' . ucwords(strtolower($options['type']));
            if (class_exists($class)) {
                self::$instance[$md5] = new $class($options);
                dump('我执行的是连接数据库');
            } else {
                // 类没有定义
                E(L('_NO_DB_DRIVER_') . ': ' . $class);
            }
        }
        ......

if (!isset(self::$instance[$md5])) { 如果不存在我们传入的连接参数才去执行后面的
self::$instance[$md5] = new $class($options); 实例操作,并且执行完一次后就赋值给刚刚说的“门牌号”,所以你只需要连接一次数据库就一直保存了这个数据库连接实例,极大减少了开销。

(写在后面:感谢thinkp3.2作者“麦当苗儿”的帮助,一开始Mysql类和Driver类的关系没有注意到是继承,一直卡在那里,要不是作者的点拨,博客可能都没有进展)

你可能感兴趣的:(thinkphp3.2源码解读)