构造并执行第一条语句。上一篇完成了代码结构的搭建和PDO的基本封装。在本文中,我们将讨论如何构造一个最基本的SQL语句并执行它来获取结果。查询sql构造目标:SELECT*FROMtest_table;querybuilder执行语法构造目标:$drivers->table('test_table')->select('\*')->get();测试数据表请大家自己搭建,这里就不单独演示了。下面我们回顾一下PDO执行这条查询语句的基本用法:1.PDO::query()方法获取结果集:$pdo->query("SELECT*FROMtest_table;");2.PDO::prepare(),PDOStatement::execute()方法:$pdoSt=$pdo->prepare("SELECT*FROMtest_table;");$pdoSt->execute();$pdoSt->fetchAll(PDO::FETCH_ASSOC);PDO::prepare()方法提供了一种防注入和参数绑定机制,可以指定结果集的返回格式,更加灵活,易于封装。我们选择这个。查询sql字符串构造:构造查询sql语句,不妨先观察一下它的基本结构:SELECT、要查找的字段(列)、FROM、要查找的表、关联子句、条件子句、分组子句、排序子句、限制条款。除了SELECT和FROM是固定的,我们只需要构造一串查询字段、表名和一系列子句,然后按照查询sql的语法拼接起来即可。在基类PDODriver.php中添加属性作为构造字符串:protected$_table='';//表名受保护$_prepare_sql='';//prepare方法执行的sql语句protected$_cols_str='*';//需要查询的字段,默认为*(all)protected$_where_str='';//where子句受保护$_orderby_str='';//orderby子句protected$_groupby_str='';//groupby子句保护$_having_str='';//having子句(与groupby一起使用)protected$_join_str='';//join子句受保护$_limit_str='';//limit子句的基本方法的创建有一个String属性的基本结构,可以开始构造一个sql。添加_buildQuery()方法构造sql字符串:protectedfunction_buildQuery(){$this->_prepare_sql='SELECT'.$this->_cols_str.'FROM'.$this->_table.$this->_join_str.$this->_where_str.$this->_groupby_str.$this->_having_str.$this->_orderby_str.$this->_limit_str;}添加table()方法设置表名:publicfunctiontable($表){$this->_table=$table;返回$这个;//对于链式调用,返回当前实例}添加select()方法,这里使用可变参数灵活处理传入:publicfunctionselect(){//获取传入方法的所有参数$cols=func_get_args();if(!func_num_args()||in_array('*',$cols)){//如果不传入参数,默认查询所有字段$this->_cols_str='*';}else{$this->_cols_str='';//清除默认*值//构造"field1,filed2..."stringforeach($colsas$col){$this->_cols_str.=''.$col.',';}$this->_cols_str=rtrim($this->_cols_str,',');}return$this;}构造并执行sql字符串构造完成,则Next,需要执行sql并取结束方式添加get()方法:publicfunctionget(){try{$this->_buildQuery();//构建sql//准备预处理$pdoSt=$this->_pdo->prepare($this->_prepare_sql);//执行$pdoSt->execute();}catch(PDOException$e){抛出$e;}返回$pdoSt->fetchAll(PDO::FETCH_ASSOC);//获取键值数组形式的结果集}测试修改test/test.php:require_oncedirname(dirname(__FILE__)).'/vendor/autoload.php';useDrivers\Mysql;$config=['host'=>'localhost','port'=>'3306','user'=>'username','password'=>'password','dbname'=>'database','charset'=>'utf8','timezone'=>'+8:00','collection'=>'utf8_general_ci','strict'=>false,];$driver=newMysql($config);//执行SELECT*FROMtest_table的查询;$results=$driver->table('test_table')->select('*')->get();var_dump($results);注意:由于上面代码中_cols_str属性默认为'*',所以查询所有字段时省略也可以调用select()方法。为了节省篇幅,一些通用方法只使用Mysql驱动类作为测试对象。对于PostgreSql和Sqlite,请读者自行测试,后面不再单独说明。优化1.解耦get方法中的prepare和execute流程是通用的(query、insert、delete、update等)。我们可以将这部分代码提取出来,在其他方法中复用,执行sql获取结果。在基类中新建一个_execute()方法:protectedfunction_execute(){try{$this->_pdoSt=$this->_pdo->prepare($this->_prepare_sql);$this->_pdoSt->execute();}catch(PDOException$e){抛出$e;}}由于逻辑分离到另一个方法中,get()方法无法获取到PDOStatement实例,所以将PDOStatement实例保存到基类的属性中:protected$_pdoSt=NULL;修改get()方法:publicfunctionget(){$this->_buildQuery();$this->_execute();return$this->_pdoSt->fetchAll(PDO::FETCH_ASSOC);}2.参数重置使用querybuilder进行查询后,每一个构造的字符串的内容都被修改了。为了不影响下一次查询,需要将这些构造好的字符串恢复到初始状态。注意:在常驻内存单例模式下,使用一个类进行多次查询是很常见的。添加_reset()方法:protectedfunction_reset(){$this->_table='';$this->_prepare_sql='';$this->_cols_str='*';$this->_where_str='';$this->_orderby_str='';$this->_groupby_str='';$this->_having_str='';$this->_join_str='';$this->_limit_str='';$this->_bind_params=[];}修改_execute()方法:protectedfunction_execute(){try{$this->_pdoSt=$this->_pdo->prepare($this->_prepare_sql);$this->_pdoSt->执行();$this->_reset();//每次执行完sql后,将每一个构造好的字符串恢复到初始状态,以保证下一次查询的正确性}catch(PDOException$e){throw$e;}}row()方法上面的get()方法直接获取了整个结果集。而有些业务逻辑只想得到一行结果,就需要一个row()方法来实现这个需求。row()方法并不难,只需将get()方法中的PDOStatement::fetchAll()方法改成PDOStatement::fetch()方法即可:publicfunctionrow(){$this->_buildQuery();$this->_execute();return$this->_pdoSt->fetch(PDO::FETCH_ASSOC);}这里就不多说了,大家可以自行测试结果。断线重连对于一个典型的web环境来说,一个sql查询已经以一个HTTP请求结束,PHP的垃圾回收功能会在一个请求周期内回收数据。而且一个HTTP请求的时间比较短,所以基本不用考虑断库的问题。但是在常驻内存的环境下,尤其是单例模式下,数据库驱动类不一定一直销毁在内存中。如果长时间没有访问数据库,数据库驱动类建立的数据库连接会被数据库作为空闲连接切断(具体时间由数据库设置决定),如果旧连接此时还在使用object,就会出现连续报错的问题。也就是说,我们要处理数据库的断开连接,当检测到断开连接时,创建一个新的连接来替换旧的连接。[1]在PDO中,如果数据库断开连接,继续访问,会相应抛出PDOException异常(也可以是错误,由PDO的错误处理设置决定)。当数据库中发生错误时,PDOException实例的errorInfo属性存储错误详细信息的数组。第一个元素返回SQLSTATE错误码,第二个元素是具体的驱动错误码,第三个元素是具体的错误信息。见PDO::errorInfoMysql有两个与断开连接相关的错误码:2006CR_SERVER_GONE_ERROR2013CR_SERVER_LOSTPostgreSql有一个与断开连接相关的错误码:具体驱动错误码为7时,PostgreSql断开连接(该驱动错误码是实测得到的PDOException,暂时没有找到相关文档)Sqlite是基于内存和文件的,没有断线,所以不考虑。这里我们以PDO具体的驱动错误码作为判断断线的依据。在基类中添加_isTimeout()方法:protectedfunction_isTimeout(PDOException$e){//如果异常信息满足断开条件,则返回truereturn($e->errorInfo[1]==2006||//MySQLserverhasgoneaway(CR_SERVER_GONE_ERROR)$e->errorInfo[1]==2013||//在查询期间失去与MySQL服务器的连接(CR_SERVER_LOST)$e->errorInfo[1]==7//没有与服务器的连接(forpostgresql));}修改_execute()方法增加断线重连函数:protectedfunction_execute(){try{$this->_pdoSt=$this->_pdo->prepare($this->_prepare_sql);$this->_pdoSt->execute();$this->_reset();}catch(PDOException$e){//PDO抛异常判断数据库是否断开if($this->_isTimeout($e)){//断开异常,清除旧数据库连接,重新连接$this->_closeConnection();$this->_connect();//重试异常前的操作try{$this->_pdoSt=$this->_pdo->prepare($this->_prepare_sql);$this->_pdoSt->execute();$this->_reset();}catch(PDOException$e){//仍然失败,抛出异常throw$e;}}else{//没有把broken引起的异常抛出,交给外部逻辑处理throw$e;}}}对了,之前暴露的PDO原生接口也支持断线重连:publicfunctionquery($sql){try{return$this->_pdo->query($sql);}catch(PDOException$e){//超时,重连if($this->_isTimeout($e)){$this->_closeConnection();$this->_connect();尝试{返回$this->_pdo->query($sql);}catch(PDOException$e){抛出$e;}}else{抛出$e;}}}publicfunctionexec($sql){try{return$this->_pdo->exec($sql);}catch(PDOException$e){//超时,重连if($this->_isTimeout($e)){$this->_closeConnection();$this->_connect();尝试{return$this->_pdo->exec($sql);}catch(PDOException$e){抛出$e;}}else{抛出$e;}publicfunctionprepare($sql,array$driver_options=[]){try{return$this->_pdo->prepare($sql,$driver_options);}catch(PDOException$e){//超时,重连if($this->_isTimeout($e)){$this->_closeConnection();$this->_connect();尝试{return$this->_pdo->prepare($sql,$driver_options);}catch(PDOException$e){抛出$e;}}else{抛出$e;}}}如何模拟断开连接?内存常驻模式下(如workerman的服务器监控环境):访问数据库,重启服务器的数据库软件,再次访问数据库,看能否正常获取数据。参考【1】Workermanmysql就可以了
