摘自文档:
在ThinkPHP中基础的模型类就是 Think\Model 类,该类完成了基本的CURD、ActiveRecord模式、连贯
操作和统计查询,一些高级特性被封装到另外的模型扩展中。
基础模型类的设计非常灵活,甚至可以无需进行任何模型定义,就可以进行相关数据表的ORM和CURD操
作,只有在需要封装单独的业务逻辑的时候,模型类才是必须被定义的。
使用模型类我们可以对相关的数据表进行CURD操作,实例化模型类的方法是我们熟悉的M或者D方法,使用模型类之前,我们来看看thinkphp是然后让模型类去对数据表进行CURD操作的。
使用M方法实例化模型
因为D方法要先去查找某个具体的模型类,所有我们从M方法中去研究tp是如何获取数据库连接实例的。
我们写下M(‘user’)运行,因为我的数据库中没有这张表,最后tp会抛出异常,在以前tp错误异常处理中也介绍过tp处理异常的方法,通过tp返回的回溯追踪我们来看看M方法的执行过程:
通过上图我们可以看出,省去中间的一些细节操作,M的操作可以看成一下四步:
/**
* 架构函数
* 取得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类的关系没有注意到是继承,一直卡在那里,要不是作者的点拨,博客可能都没有进展)