一直觉得TP框架的数据库查询链式操作很有意思,闲来无事想去一探究竟,他是怎么实现的数据库查询呢?
TP5的目录结构跟TP3比大相径庭。简单介绍一下TP5的主目录结构:
├─application 应用目录
├─extend 扩展类库目录(可定义)
├─public 网站对外访问目录
├─runtime 运行时目录(可定义)
├─vendor 第三方类库目录(Composer)
├─thinkphp 框架核心目录
├─build.php 自动生成定义文件(参考)
├─composer.json Composer定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行工具入口
而数据库操作类是在thinkphp目录下面,我们用到的文件(已加粗)包括:
├─thinkphp
├──library
├───think
├────db
├─────bulider
├─────connector
├─────exception
├─────Builder.php
├─────Connection.php
├─────Query.php
├────Db.php
打开各个文件看,在Connection.php 和 Builder.php中定义了抽象类,而同名文件夹下的各个数据库对应类均引用了这个抽象类,从文件名不难理解Connection应该是做数据库连接的,而builder应该是做数据库sql语句处理的。
在use Db类之后,随便写一个简单的查询语句:
Db::table(‘persons’)->select();
我们看一下程序的执行是怎样的。
查看代码Query.php这个文件use引用了db类的所有文件,数据库查询即从这个文件开始。
当创建Db对象的时候,程序先执行了Query.php的构造方法:
public function __construct(Connection $connection = null, $model = null)
{
$this->connection = $connection ?: Db::connect([], true);
$this->prefix = $this->connection->getConfig('prefix');
$this->model = $model;
// 设置当前连接的Builder对象
$this->setBuilder();
}
一、构造方法中调用了Db类中的connect方法,并把结果赋给connection属性。
从connection中调用getConfig方法得到前缀信息赋值给prefix属性;
先看一下Db类中的connect方法做了哪些操作:
public static function connect($config = [], $name = false)
{
if (false === $name) {
$name = md5(serialize($config));
}
if (true === $name || !isset(self::$instance[$name])) {
// 解析连接参数 支持数组和字符串
$options = self::parseConfig($config);
if (empty($options['type'])) {
throw new \InvalidArgumentException('Undefined db type');
}
$class = false !== strpos($options['type'], '\\') ?
$options['type'] :
'\\think\\db\\connector\\' . ucwords($options['type']);
// 记录初始化信息
if (App::$debug) {
Log::record('[ DB ] INIT ' . $options['type'], 'info');
}
if (true === $name) {
$name = md5(serialize($config));
}
self::$instance[$name] = new $class($options);
}
return self::$instance[$name];
}
private static function parseConfig($config)
{
if (empty($config)) {
$config = Config::get('database');
} elseif (is_string($config) && false === strpos($config, '/')) {
$config = Config::get($config); // 支持读取配置参数
}
return is_string($config) ? self::parseDsn($config) : $config;
}
此方法又调用本类的parseConfig方法,parseConfig方法则从Config类中获取database数据,从config数组中的type字段获得类名,Config类是手动填写在config文件中的一些配置信息数组,type这里填的是mysql,所以如果填写的是其他数据库名称,则会创建其对应数据库的类;
并将config数组作为初始化参数传入对应类的构造函数中,最后将这个对象返回。
那么这里的类名是什么呢?打印出来看一下:
\think\db\connector\Mysql
connector文件夹下的mysql.php继承了Connection.php,Connection.php的构造函数中将之前传入的配置参数赋值给了config属性。
二、调用Builder类
上面的一系列操作并没有真正的连接数据库,我们从Db的connect方法中得到了connection类的实体对象,并且赋值给了Query类下面的connection属性;
而Query的构造函数第一个递归函数执行完毕,马上调用了本类中的setBuilder方法;
那么setBuilder方法做了些什么呢?先看代码:
protected function setBuilder()
{
$class = $this->connection->getBuilder();
$this->builder = new $class($this->connection, $this);
}
他调用了connection实体中的getBuilder方法,得到class并将其实例化,而参数是 this。将对象赋值给了builder属性
public function getBuilder()
{
if (!empty($this->builder)) {
return $this->builder;
} else {
return $this->getConfig('builder') ?: '\\think\\db\\builder\\' . ucfirst($this->getConfig('type'));
}
}
到现在就会发现 之前有个bulider类一直没有用到,会不会是调用他了呢,那么数据库是从什么时候开始连接的?
再次打印class,猜的没错,这次创建了bulider对象:
\think\db\builder\Mysql
打开bulider文件,可以看到这里面定义了 增删改查 等操作的SQL表达式,还有一些对SQL字符串的处理方法。
三、初始化操作完成,得到包含connection和builder实体的Query对象
经过以上的这些操作,Query类中connection属性和bulider属性均保存了对应的实体,这样程序可以在query执行时调用到他们的各种方法。而我们的查询语句Db::table(‘persons’)->select();算是完成了Db::这部分的初始化处理。
现在调用Query类中的table方法:
public function table($table)
{
if (is_string($table)) {
if (strpos($table, ')')) {
// 子查询
} elseif (strpos($table, ',')) {
$tables = explode(',', $table);
$table = [];
foreach ($tables as $item) {
list($item, $alias) = explode(' ', trim($item));
if ($alias) {
$this->alias([$item => $alias]);
$table[$item] = $alias;
} else {
$table[] = $item;
}
}
} elseif (strpos($table, ' ')) {
list($table, $alias) = explode(' ', $table);
$table = [$table => $alias];
$this->alias($table);
}
} else {
$tables = $table;
$table = [];
foreach ($tables as $key => $val) {
if (is_numeric($key)) {
$table[] = $val;
} else {
$this->alias([$key => $val]);
$table[$key] = $val;
}
}
}
$this->options['table'] = $table;
return $this;
}
将实体this返回实现链式操作,然后继续执行Query下面的select方法。
但是一直有个问题,其实到现在为止都还没有做数据库连接,那么到底是什么时候连接数据库的呢,继续往下看!
select方法也是很长很长,但是在里面我找到了这样一句话:
$resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_pdo']);
前面对我们传过来的各项数组或者字符串进行处理并拼接成了sql字符串,在这里调用了query方法,看一下query方法是怎么写的:
public function query($sql, $bind = [], $master = false, $class = false)
{
return $this->connection->query($sql, $bind, $master, $class);
}
当看到这里的connection的时候我觉得已经有点眉目了(connection类是做数据库连接的)
那么connection下面一定有个query方法,再去看connection类(讲道理类虽然不多,然而还是会有点晕!)
public function query($sql, $bind = [], $master = false, $pdo = false)
{
$this->initConnect($master);
if (!$this->linkID) {
return false;
}
/*下面代码太多,省略不看了*/
}
其实这里调用的initConnect从字面上已经能看出来,这个方法就是做数据库连接的了,其实下面的所有数据库操作方法(比如删、改、开启事务)前面都有这样一个init方法:
protected function initConnect($master = true)
{
if (!empty($this->config['deploy'])) {
// 采用分布式数据库
if ($master || $this->transTimes) {
if (!$this->linkWrite) {
$this->linkWrite = $this->multiConnect(true);
}
$this->linkID = $this->linkWrite;
} else {
if (!$this->linkRead) {
$this->linkRead = $this->multiConnect(false);
}
$this->linkID = $this->linkRead;
}
} elseif (!$this->linkID) {
// 默认单数据库
$this->linkID = $this->connect();
}
}
public function connect(array $config = [], $linkNum = 0, $autoConnection = false)
{
if (!isset($this->links[$linkNum])) {
if (!$config) {
$config = $this->config;
} else {
$config = array_merge($this->config, $config);
}
// 连接参数
if (isset($config['params']) && is_array($config['params'])) {
$params = $config['params'] + $this->params;
} else {
$params = $this->params;
}
// 记录当前字段属性大小写设置
$this->attrCase = $params[PDO::ATTR_CASE];
// 数据返回类型
if (isset($config['result_type'])) {
$this->fetchType = $config['result_type'];
}
try {
if (empty($config['dsn'])) {
$config['dsn'] = $this->parseDsn($config);
}
if ($config['debug']) {
$startTime = microtime(true);
}
$this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params);
if ($config['debug']) {
// 记录数据库连接信息
Log::record('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn'], 'sql');
}
} catch (\PDOException $e) {
if ($autoConnection) {
Log::record($e->getMessage(), 'error');
return $this->connect($autoConnection, $linkNum);
} else {
throw $e;
}
}
}
return $this->links[$linkNum];
}
至此数据库连接和查询都执行完毕了,return结果,最后就是我们想看到的了。