在ThinkPHP5.0的框架使用过程中,Db类是一定会接触到的,上手不难,但若想随心所欲的用,还是需要了解一番。比如我记忆力不太好,在开发过程中添加where,若出现like,eq等等条件,老是忘了后面是跟数组呢还是逗号分割;再使用join时,想要LEFT或RIGHT时,又忘了如何注明。迫不得己,用一次看一次文档。在之前零零碎碎的源码学习过程中,虽说也记不起许多细节,也说不出到底学了什么,但总归自己觉得有了进步,起码对TP5这个框架使用更加得心应手,毕竟技术服务于业务,能够写出更简介、更方便、更有效的业务代码,本身就是一件身心愉悦的事儿。
Db::table(‘t_user’)->where(‘id’, 1)->find();
查询t_user表中id等于1的行,所以将目光聚焦thinkphp/library/think/Db.php文件。
PHPStorm提示
先说个和业务无关的题外话,Db类内部没有找到table()静态方法,可是我们在开发时,输入Db+两个引号,会弹出多个方法提示,其中包括了table()。这就觉得有点奇怪了,打开Db代码,发现包含了大堆对method的注释。我照着新增@method注解后,自定义方式IDE也会提示了!真是个好玩意儿,比如我喜欢把redis封装成单例工具类。那么再某些情况下,实例对象调用方法时,调试上不太友好。基于Db的类注释,刚刚已经确认能够在开发中增加提示,让开发过程更舒服。
/**
* Class Db
* @package think
* @method Query table(string $table) static 指定数据表(含前缀)
* @method Query name(string $name) static 指定数据表(不含前缀)
* @method Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件
* @method Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询
* @method Query union(mixed $union, boolean $all = false) static UNION查询
* @method Query limit(mixed $offset, integer $length = null) static 查询LIMIT
* @method Query order(mixed $field, string $order = null) static 查询ORDER
* @method Query cache(mixed $key = null , integer $expire = null) static 设置查询缓存
* @method mixed value(string $field) static 获取某个字段的值
* @method array column(string $field, string $key = '') static 获取某个列的值
* @method Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询
* @method mixed find(mixed $data = null) static 查询单个记录
* @method mixed select(mixed $data = null) static 查询多个记录
* @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录
* @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID
* @method integer insertAll(array $dataSet) static 插入多条记录
* @method integer update(array $data) static 更新记录
* @method integer delete(mixed $data = null) static 删除记录
* @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据
* @method mixed query(string $sql, array $bind = [], boolean $master = false, bool $pdo = false) static SQL查询
* @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行
* @method Paginator paginate(integer $listRows = 15, mixed $simple = null, array $config = []) static 分页查询
* @method mixed transaction(callable $callback) static 执行数据库事务
* @method void startTrans() static 启动事务
* @method void commit() static 用于非自动提交状态下面的查询提交
* @method void rollback() static 事务回滚
* @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句
* @method string quote(string $str) static SQL指令安全过滤
* @method string getLastInsID($sequence = null) static 获取最近插入的ID
*/
回归正文
TP5对SQL操作根据功能块上的区分,封装成几个不同模块。一句简单的查询,其实大有讲究。
变量名 | 含义 | 变量类型 |
---|---|---|
$instance | 数据库连接实例 | Connection[] |
$queryTimes | 查询次数 | int |
$executeTimes | 执行次数 | int |
清除连接实例
public static function clear()
数据库连接参数解析
private static function parseConfig($config)
若传入$config变量为empty,则获取全局配置database,配置位置来自application/database.php,官方文档告诉我们这是数据库配置文件。
DSN 解析
private static function parseDsn($dsnStr)
如果传入dsn格式:mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8
将解析按database.php内的数组格式进行封装。
/**
* 调用驱动类的方法
* @access public
* @param string $method 方法名
* @param array $params 参数
* @return mixed
*/
public static function __callStatic($method, $params)
{
return call_user_func_array([self::connect(), $method], $params);
}
来看看_callStatic的实现,它通过call_user_func_array()将本身变成了一个中转站,将各类条件方法传递给(self::connect()对象)。
毫无疑问__callStatic()和connect()方法非常重要,当执行Db::table()时,触发__callStatic(),
在执行call_user_func_array前通过self::connect()获取数据库连接实例。所以table()与Db类并无事实关联关系,我们完全可以更换代码Db::connect()->table()执行,当然这麻烦了不少。
/**
* 数据库初始化,并取得数据库类实例
* @access public
* @param mixed $config 连接配置
* @param bool|string $name 连接标识 true 强制重新连接
* @return Connection
* @throws Exception
*/
public static function connect($config = [], $name = false)
connect()解析了数据库连接配置参数,并创建数据库类实例。基于此,我们可以不停的调用table、where、find、select等等方法来执行各种SQL。
要注意三个点
1.默认情况下实例化数据库连接对象:\think\db\connector\Mysql
2.$name变量值决定每次调用是否重新创建连接实例。这意味着每次请求即重新连接或以单例模式实现复用
Db类对外提供的功能很简单,统计脚本生命周期内SQL查询总次数和SQL查询总次数;创建、维护、返回数据库连接实例。作> 为开发人员,拿到数据库实例,就可以开始与Mysql的通信。
3.单例或切换数据库连接,取决于传入的$config配置数组,以其序列化结果来作为参照,感受到了一丝hash的韵味。
研究下数据库连接实例又是如何与Mysql通信的
切入点:\think\db\connector\Mysql extends Connection
connector\Mysql
属性:
protected $builder = '\\think\\db\\builder\\Mysql';
对应一个类名,builder创建具体的SQL语句
方法:
解析pdo连接的dsn信息
parseDsn($config)
拼接PDO-Mysql连接所需要的dsn内容
取得数据表的字段信息
getFields($tableName)
执行SHOW COLUMNS FROM tableName语句,获取指定表的字段信息。内容类型:Field(字段名)、Type(字段类型)、Null(是否空)、Default(默认值)、Key(键类型,如PRI主键;MUL索引)、extra(备注信息,如auto_increment为自增)
获取库内表信息
getTables($dbName = '')
SQL性能分析
getExplain($sql)
执行EXPLAIN ($sql),获取执行分析结果
supportSavepoint()
判断是否支持事务嵌套
对比connector目录下的其它文件,属性和方法的实现截然不同。因此,builder对应不同数据库的SQL拼接,其他方法实现了各自的自定义配置。
关注PDO连接参数的作用
// PDO连接参数
$params = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
];
PDO::ATTR_CASE 强制列名为指定的大小写
=> PDO::CASE_LOWER:强制列名小写。
=> PDO::CASE_NATURAL:保留数据库驱动返回的列名。
=> PDO::CASE_UPPER:强制列名大写。
PDO::ATTR_ORACLE_NULLS 转换 NULL 和空字符串
=> PDO::NULL_NATURAL: 不转换。
=> PDO::NULL_EMPTY_STRING: 将空字符串转换成 NULL。
=> PDO::NULL_TO_STRING: 将 NULL 转换成空字符串。
PDO::ATTR_ERRMODE 设置抛出异常
PDO::ATTR_STRINGIFY_FETCHES 提取的时候将数值转换为字符串
PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟
Connection是一个抽象类,内部已实现方法都是与数据库操作相关,观察其属性都是PDO的一些配置及实例,包含的内容比较大,所以就几个常用的功能描述下。
- 实例化Query对象,执行链式方法,如table()、where()、find()。每次执行都会新实例化,这也导致了第一句sql执行后,想再执行必须重新附加上条件,据作者说5.1版本会保留条件,个人觉得5.0的初衷是为了实现model的功能而忽略了这个点。
- 创建单例Mysql连接,分布式环境下若从库连接失败会自动连接主库。
- query()执行查询,返回结果集
- execute()执行语句,返回影响行数
- 事务处理
- 批量执行SQL,自动启动事务支持,需将SQl存入到数组内传入
- 获取最近插入的ID
- 获取最近的错误信息,封装PDOStatement的error
- SQL执行时间记录、性能分析、事件监听
- 获取最近执行的SQL
一直以来犯了一个错误,每次执行后想获取完整的SQL语句,又再执行了Db::table(tableName)->getLastSql()。如此调用导致无意义的重新实例化Query类并调用table()方法,Query内的getLastSql()会去调用Connection的getLastSql()。本来Db=>Connection=> getLastSql可达到目的,而错误的Db=>Connection=>new Query=>Connection’s getLastSql。
从时间和空间上都造成了浪费。
本来重点想研究Query,不过稍微了解后,兴趣度稍微没这么大了。作者实现了一个全面的框架,当然是一个非常厉害的人,他将非常多的操作简化,比如比较时间、单条查询转一维数组、SQL执行事件响应等等,这样我们能够将SQL执行注意集中点转移到数据上,一条数据新增即封装一维数组,多条就二维,想要获取单个值就执行value(),获取单列就执行column(),执行多条新增就insertAll,我们忽略事务概念只需要判断结果。当然,有好处也既有牺牲,经过层层封装,代码效率势必会降低不少。以后最好就基于Db::query()和Db::execute()来吧,甚至自己新封装一个简化版PDO工具类也成。
来概述下Query的思路
Query维护一个全局options属性,存储查询参数。比如where、limit、order、group等等。在执行find()、select()等触发查询的方法时,会将options内的参数组装,Builder的select模版如:SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT%%LOCK%%COMMENT%,按照参数类型作对应替换,最后生成一条完整SQL。
关于数据库这块的学习,很难说出具体的总结,上文中的流程图可以大概描述了。在一些编程的细节和思想里,确实有发现不少觉得眼前一亮的技巧,再慢慢琢磨吧!