数据查询描述软件系统中数据库或者持久层的基本操作功能, 主要包括增加(Create)、查询(Retrieve)、更新(Update)和删除(Delete), 统称为CRUD.
MySQL是一个小型关系型数据库管理系统,由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,许多中小型网站选择了MySQL作为网站数据库。在phpwind论坛系统中.
Phpwind论坛系统采用的就是MYSQL作为网站数据库, 并且将mysql的查询语言封装, 使原本繁杂的操作语言变得简单. 封装的类分为两种, 分别在require/db_mysql.php和require/db_mysqli.php. 这两种类是根据连接方式区分的. 在这两个文件之上, 又有一个文件对他们进行包装, 根据论坛的配置文件选择其一进行连接, 可以查看
require/db_connects.php,
这里介绍下两种连接方式的区别:
mysql是非持继连接函数而mysqli是永远连接函数。也就是说
mysql每次链接都会打开一个连接的进程而mysqli将使用同一连接进程, 从而减少了服务器的开销, mysqli不是默认开启的,
windows系统下要把php.ini里面php_mysqli.dll前的;号去掉. linux下要把mysqli编译进去. mysqli兼容性更好, 效率性能更稳定, 与mysql连接更稳定. 若服务器的配置是PHP5.1.0或更高版本和MySQL4.1.3或更高版本, 可以尝试使用 mysqli.
这里我们主要已mysql方式作为演示, mysqli的用法是一样的.
Global.php文件里面有实例化一个$db变量, 这个变量是require/db_ connects.php文件定义的类实例, 具体代码在266行
PwNewDB();
PwNewDB函数的定义在require/common.php文件里
function PwNewDB()
{
if (!is_object($GLOBALS['db'])) {
global $db, $database, $dbhost, $dbuser, $dbpw, $dbname, $PW, $charset, $pconnect;
require_once S::escapePath(R_P . "require/db_$database.php");
$db = new DB($dbhost, $dbuser, $dbpw, $dbname, $PW, $charset, $pconnect);
}
}
可以看到, 这个函数实例化已一个全局变量$db, 这样, 只要文件里面包含了global.php, 就能非常方便的使用$db变量操作数据库. 在论坛的代码中随处可见$db->query , 就是这样!
现在主要介绍下包装后的类如何方便快捷的执行数据库操作:
在介绍这些方法之前, 请先弄明白一些原理, 实际上这些封装的方法都会调用统一方法去操作数据库, 在$db实例中, 这个统一的方法就是query, 下面我们来看下query方法的定义:
function query($SQL, $method = null, $error = true) {
$originalSQL = $SQL;
if ($this->dbpre != 'pw_') {
$SQL = str_replace(array(' pw_', '`pw_', " 'pw_"),array(" $this->dbpre", "`$this->dbpre", " '$this->dbpre"), $SQL);
}
$GLOBALS['db_debug'] && list($begintime, $begintime_sec) = explode(" ", microtime());
if ($method && function_exists('mysql_unbuffered_query')) {
$query = @mysql_unbuffered_query($SQL, $this->sql);
} else {
$query = @mysql_query($SQL, $this->sql);
}
if (in_array(mysql_errno($this->sql), array(2006, 2013))
&& empty($query) && $this->pconnect == 0 && !defined('QUERY')) {
define('QUERY', true);
@mysql_close($this->sql);
sleep(2);
$this->connect();
$query = $this->query($SQL);
}
$GLOBALS['db_debug'] && list($endtime, $endtime_sec) = explode(" ", microtime());
$method <> 'U' && $this->query_num++;
if ($GLOBALS['db_debug']) {
$usetime = $endtime + $endtime_sec - $begintime - $begintime_sec;
$this->arr_query .=
$SQL . "\t\ttime:" . $usetime . "\r\n\r\n";
$this->totaltime += $usetime;
}
!$query && $error && $this->halt('Query Error: ' . $SQL);
require_once(R_P.'lib/cloudwind/cloudwind.class.php');
CloudWind::yunCollectSQL($originalSQL);
return $query;
}
不需要仔细看懂这里面的每行代码, 只需要了解封装是原理的, 可以看到, 上面代码的黄色部分, 就是调用php原生操作数据库的方法. 这里有两种方法: mysql_unbuffered_query和mysql_query, 这两种都是php自带的方法, 主要区别是: mysql_unbuffered_query() 向 MySQL 发送一条 SQL 查询 query, 但不像 mysql_query() 那样自动获取并缓存结果集. 一方面, 这在处理很大的结果集时会节省可观的内存. 另一方面, 可以在获取第一行后立即对结果集进行操作, 而不用等到整个 SQL 语句都执行完毕. mysql_unbuffered_query() 的好处是有代价的: 在mysql_unbuffered_query() 返回的结果集之上不能使用 mysql_num_rows() 和 mysql_data_seek(). 此外在向 MySQL 发送一条新的 SQL 查询之前, 必须提取掉所有未缓存的 SQL 查询所产生的结果行.在query方式之上, 又还有一些其他更方便的方法封装了query方法, 比如很常用的update方法
function update($SQL, $lp = 1) {
if ($this->lp == 1 && $lp) {
$tmpsql6 = substr($SQL, 0, 6);
$SQL = strtoupper($tmpsql6.'E') == 'REPLACE' ? 'REPLACE LOW_PRIORITY' . substr($SQL,7) : $tmpsql6 . ' LOW_PRIORITY' . substr($SQL,6);
}
return $this->query($SQL, 'U');
}
以上都是比较底层的代码, 其实我们使用过程中很简单, 下面会一一介绍.
1、增加(Create)
增加: 指的是对数据库添加的操作, 包括增加数据库, 数据表, 数据记录. 以后论坛代码始终使用唯一的数据库($db实例已经指定了这个库), 所以我们封装的代码里面没有增加数据库的代码. 增加数据库的操作一般也很少会用到, 下面的代码展示了如何在程序代码中增加数据表:
$createsql = "CREATE TABLE ".getActivityValueTableNameByActmid($actmid, 0, 1)." (tid mediumint(8)
unsigned NOT NULL,fid SMALLINT( 6 ) UNSIGNED NOT NULL
DEFAULT 0,ifrecycle TINYINT(1) NOT NULL DEFAULT 0,PRIMARY KEY (tid))";
if ($db->server_info() >= '4.1') {
$createsql .= " ENGINE=MyISAM".($charset ? " DEFAULT CHARSET=$charset" : '');
} else {
$createsql .= " TYPE=MyISAM";
}
$db->query($createsql);
可以看到调用的是query方法, 只需要把建表的sql语句写好, 传递给query方法即可.
下面再来看下插入新的记录的代码:
$sqlarray = array(
'fupid' =>
$actuid,
'tid' =>
$tid,
'uid' =>
$memberdb['uid'],
'actmid' =>
$memberdb['actmid'],
'username' =>
$memberdb['username'],
'totalcash' =>
$totalcost,
'signuptime' =>
$timestamp,
'isadditional' =>
1,
'additionalreason' => $additionalreason,
);
$db->update("INSERT INTO pw_activitymembers SET " . S::sqlSingle($sqlarray));
S::sqlSingle后面会做介绍.插入记录都是使用update方法, 其实也可以使用query方法, 只不过稍微麻烦点, 细心观察两个方法的定义(db_connects.php里面):
function update($SQL, $lp = 1) {
$mastdb = $this->getMastdb();
return $mastdb->update($SQL, $lp);
}
function query($SQL, $method = null, $error = true) {
$GLOBALS['db_debug'] && list($begintime, $begintime_sec) = explode(" ", microtime());
$slavedb = $this->getSlavedb();
$mastdb = $this->getMastdb();
$query = $this->judgesql($SQL) == 1 ? $slavedb->query($SQL, $method, $error) : $mastdb->query($SQL, $method, $error);
$method != 'U' && $this->query_num++;
if ($GLOBALS['db_debug']) {
list($endtime, $endtime_sec) = explode(" ", microtime());
$usetime = $endtime + $endtime_sec - $begintime - $begintime_sec;
$this->arr_query .=
$SQL . "\t\ttime:" . $usetime . "\r\n\r\n";
$this->totaltime += $usetime;
}
return $query;
}
可以发现, 封装的函数有明确的分工, update主要负责更新数据表的操作, query则是主要负责查询, 如果你还想继续深入了解这两个的区别, 你就会看到db_mysql.php里面query和update方法的定义了:
function update($SQL, $lp = 1) {
if ($this->lp == 1 && $lp) {
$tmpsql6 = substr($SQL, 0, 6);
$SQL = strtoupper($tmpsql6.'E') == 'REPLACE' ? 'REPLACE LOW_PRIORITY' . substr($SQL,7) : $tmpsql6 . ' LOW_PRIORITY' . substr($SQL,6);
}
return $this->query($SQL, 'U');
}
function query($SQL, $method = null, $error = true) {
$originalSQL = $SQL;
if ($this->dbpre != 'pw_') {
$SQL = str_replace(array(' pw_', '`pw_', " 'pw_"),array(" $this->dbpre", "`$this->dbpre", " '$this->dbpre"), $SQL);
}
$GLOBALS['db_debug'] && list($begintime, $begintime_sec) = explode(" ", microtime());
if ($method && function_exists('mysql_unbuffered_query')) {
$query = @mysql_unbuffered_query($SQL, $this->sql);
} else {
$query = @mysql_query($SQL, $this->sql);
}
if (in_array(mysql_errno($this->sql), array(2006, 2013))
&& empty($query) && $this->pconnect == 0 && !defined('QUERY')) {
define('QUERY', true);
@mysql_close($this->sql);
sleep(2);
$this->connect();
$query = $this->query($SQL);
}
$GLOBALS['db_debug'] && list($endtime, $endtime_sec) = explode(" ", microtime());
$method <> 'U' && $this->query_num++;
if ($GLOBALS['db_debug']) {
$usetime = $endtime + $endtime_sec - $begintime - $begintime_sec;
$this->arr_query .=
$SQL . "\t\ttime:" . $usetime . "\r\n\r\n";
$this->totaltime += $usetime;
}
!$query && $error && $this->halt('Query Error: ' . $SQL);
require_once(R_P.'lib/cloudwind/cloudwind.class.php');
CloudWind::yunCollectSQL($originalSQL);
return $query;
}
如前面所说, 最终还是会调用到query方法里面(最底层封装的).
下来一个问题, 刚才通过update方法插入了一条记录, 我需要使用这条新记录的ID怎么办? 这久需要使用到封装的insert_id方法
$db->update("INSERT INTO
pw_activitymembers SET " . S::sqlSingle($sqlarray));
$actuid = $db->insert_id();
2、查询(Retrieve)
数据库使用最多的语句就是查询了, select * from ***
前面提到过, query方法主要是负责查询的, 那么, 在编写代码是需要查询数据库变得很简单, 只需要把写好的sql语句传递给query方法即可, 下面展示最常见的查询代码:
$query = $db->query("SELECT tid,subject FROM pw_threads FORCE INDEX(".getForceIndex('idx_postdate').") $sql $orderby".S::sqlLimit(0,$forumset['relatednums']));
while ($rt = $db->fetch_array($query)) {
$rt['title'] = $rt['subject'];
$rt['url'] = "read.php?tid=".$rt['tid'];
unset($rt['subject']);
unset($rt['tid']);
$relatedb[] = $rt;
}
注意下面的$db->fetch_array, 前面的sql语句可能会取出来很多条记录, 而fetch_array则是逐条处理这些记录的方法, 请记住这个典型的while语句, 在很多地方都会发现它.
有些时候我们可能想知道, 我的一条select语句取到了多少条记录? 当然我们可以使用select
COUNT这样的语句获得答案, 但有时候我不仅仅是想知道总数, 每条记录我都有用, 那么, 使用num_rows方法吧
$query = $db->query("SELECT * FROM pw_draft
WHERE uid=" . S::sqlEscape($winduid) . $limit);
if ($db->num_rows($query) == 0) {
Showmsg('draft_error');
}
同理, 还有num_fields方法, 取得结果集中字段的数目, 这个方法一般使用到很少.
上面的代码展示了最常用查询多条记录的办法, 那有时候我们只需要查询一条记录, 难道也要写这么复杂的while循环? 当然不用, 我们的类有专门的封装方法处理这类需求, get_one, 从字面一次就很清晰的看出它的作用, 看看典型的取一条记录的代码:
$read = $db->get_one("SELECT * FROM pw_threads WHERE tid=" . S::sqlEscape($tid));
以上代码中, $read变量将包含所有这条记录的信息, 可以直接使用, 就不需要通过while循环处理过才能用了. 比如$read[‘tid’]
还有些情况, 我想查找某一特定的值, 也有相应的封装方法get_value, 这个方法非常方便, 一行代码就能让你从数据库中取出某个特定值
$author = $db->get_value('SELECT authorid FROM pw_threads WHERE tid = ' . S::sqlEscape($tid));
$isAdminright = $postActForBbs->getAdminRight($author);
get_value以后就能直接使用了.
3、更新(Update)
更新指的是对数据表里已有的记录进行修改更新操作. 比如更新某个帖子的回复数, 更新会员金币数等等.
如前面插入数据库, 更新操作基本也都是使用update方法, 下面截图说明一些典型的例子:
$db->update('UPDATE pw_hits_threads SET hits=hits+1 WHERE
tid='.S::sqlEscape($tid));
上面代码作用是更新帖子阅读数.
可能有时候我想替换某条记录, 也很简单
$db->update('REPLACE INTO pw_hits_threads SET
hits='. S::sqlEscape($hits) . ' , tid='.S::sqlEscape($tid));
跟更新动作息息相关的一个信息就是, 我刚才更新了多少条记录, 影响了多少行? 这个可以使用封装的affected_rows方法
pwQuery::update('pw_threads', 'tid=:tid
AND fid=:fid AND ifcheck=:ifcheck', array($tid,$fid,0), array('ifcheck'=>1));
if ($db->affected_rows() > 0) {}
下面介绍一种很常见的情况, 想更新一条记录, 但是不确定这条记录在数据库中是否存在, 怎么办? 一般的方法, 先查询一下这条记录, 在根据查询结果决定是update还是insert, 写下来好长一段, 在phpwind代码里, 也有专门封装的的方法来处理这种情况, pw_update, 先来看看它的定义
function pw_update($SQL_1, $SQL_2, $SQL_3) {
$rt = $this->get_one($SQL_1, MYSQL_NUM);
if (isset($rt[0])) {
$this->update($SQL_2);
} else {
$this->update($SQL_3);
}
}
不用多说也能看明白他的作用吧. $SQL_1是条件查询语句, 一般是取某个特定值的sql语句, $SQL_2是所取值存在时候执行的更新语句, $SQL_3是所取值不存在时候执行的插入语句.
4、删除(Delete)
删除数据也会经常用到, 比如违规的帖子, 取消关注这些操作在代码层面都会涉及到删除数据的动作, 这也没什么特别之处, 只需要把写好的delete语句传给query就可以了
if ($del) {
$db->update("DELETE FROM pw_attachs
WHERE aid IN(" . S::sqlImplode($del) . ')');
}
上面的代码作用是批量删除附件.
上面系统地讲述了CRUD在phpwind代码系统中的实现, 可能大家在看的过程中经常碰到那么不是很明白作用的函数, 比如S:: sqlEscape这样的, 下面讲解下这些函数的作用, 打开require/security.php,
从这个名字就能猜出这些函数的作用了, 为了安全! 仔细说来就是防止sql注入式攻击而做的安全过滤工作.
security.php里面定义了很多过滤函数, 这里我们只接受和数据库操作有关的那些, 很多函数除了过滤外, 还能使你的sql代码写的很清新, 很方便.
1.
最常见的sqlEscape, 通用多类型混合转义函数, 简单来说就是把变量变成sql语句中特有的格式. 具体细节就是把变量里面的’,”这类符号前面加上\, 然后把整个变量值用’’装起来, 防止代码里面包含的符号提前结束sql语句, 形成sql注入攻击.
$read = $db->get_one("SELECT * FROM pw_threads WHERE
tid=".S::sqlEscape($tid));
2. sqlImplode, 这个函数是用来通过","字符连接数组转换的字符, 比如我们想查找tid为1, 2, 3, 4的帖子, 我目前有个数组$selids = array(1, 2, 3, 4), 我就可以很方便的通过这个数组生成查询语句, 这样不仅代码很清晰, 而已也保证安全
$query = $db->query("SELECT tid,fid,postdate,author,authorid,
subject,topped,toolfield,specialsort FROM pw_threads WHERE tid IN(" . S::sqlImplode($selids) . ")");
3. sqlLimit, 组装SQL查询的限制条件, 只需要传入起始值, 取多少项可以了
$limit = S::sqlLimit($start_limit,$topped_page_num);
$query = $db->query("SELECT t.floor, p.* $fieldadd FROM pw_poststopped t
LEFT
JOIN $pw_posts p ON t.pid = p.pid $tablaadd
WHERE t.tid = ".S::sqlEscape($tid)." AND t.fid = '0' AND t.pid != '0' AND p.ifcheck = '1' ORDER BY t.uptime desc $limit");
4. sqlMulti, 组装多条 key=value 形式的SQL查询语句, 主要用于insert多条记录
$sqlarray[] = array(
'fupid' =>
0,
'tid' =>
$tid,
'uid' =>
$uid,
'actmid' =>
$actmid,
'username' =>
$username,
'totalcash' =>
$cost_[$uid],
'signuptime' =>
$timestamp,
'isadditional' =>
1,
'additionalreason' => $additionalreason,
);
$db->update("INSERT INTO pw_activitymembers (fupid,tid,uid,actmid,username,totalcash,signuptime,isadditional,additionalreason) VALUES " . S::sqlMulti($sqlarray));
以上例子中$sqlarray数组有很多项, 每一项都包含一组数据, 这条INSERT语句可以一次性插入多条记录.
5. sqlSingle, 与sqlMulti对应, 组装单条 key=value 形式的SQL查询语句值, 经常用于 insert一条记录或者update某条记录.
$threadInfo = array(
'fid' => $fid,
'author' => $postInfo['author'],
'authorid' => $postInfo['authorid'],
'ifcheck' => $postInfo['ifcheck'],
'postdate' => $postInfo['postdate'],
'lastpost' => $postInfo['postdate'],
'lastposter'=> $postInfo['author']
);
$db->update("UPDATE pw_threads SET " . S::sqlSingle($threadInfo)." WHERE tid = ".S::sqlEscape($tid));
下面在介绍下另外一种操作数据库的类, pwQuery
在require/common.php中定义, 这个类实际上是对require/db_connects.php里面定义的类方法再次封装, 更加直观, 这里就不做过多的解释, 只介绍一下使用方法. 以insert为例, 其他类推.
$pwSQL = array(
'fid' => $to_id,
'icon' => $icon,
'titlefont' => ''/*$titlefont*/,
'author' => $author,
'authorid' => $authorid,
'subject' => $subject,
'ifcheck' => $ifcheck,
'type' => $to_threadcate,
'postdate' => $postdate,
'lastpost' => ($lastpost > $timestamp ? $timestamp : $lastpost),
'lastposter' => $lastposter,
'hits' => $hits,
'replies' => $replies,
'topped' => 0/*$topped*/,
'locked' => $locked,
'digest' => 0/*$digest*/,
'special' => $special,
'state' => $state,
'ifupload' => $ifupload,
'ifmail' => $ifmail,
'ifshield' => $ifshield,
'anonymous' => $anonymous,
'ptable' => $db_ptable,
'modelid' => $modelid
);
//$db->update("INSERT
INTO pw_threads SET ".S::sqlSingle($pwSQL));
//$newtid
= $db->insert_id();
$newtid = pwQuery::insert('pw_threads', $pwSQL);
上面代码有两条注释的代码, 实现的效果和最下面一条是一样的, 这是pwQuery的一点小改进, 可以直接获得insert记录的id值
那么pwQuery::insert怎么使用呢? 非常简单, 第一个参数传入需要插入的表名称, 第二个参数传入键值对数组(注意:键的值必须和数据库的字段对应).
update方法有4个参数, 多出来的2个参数是用来组装where部分
pwQuery::update('pw_threads', 'tid=:tid AND fid=:fid AND ifcheck=:ifcheck', array($tid,$fid,0), array('ifcheck'=>1));
if ($db->affected_rows() > 0) {}
看到’tid=:tid
AND fid=:fid AND ifcheck=:ifcheck’这个参数, 这是where的条件, 其中:tid这类符号是代值, 用来替代真正的值
而array($tid, $fid, 0)这个参数就是真正的值了, 分别对应上面的:tid, :fid, :ifcheck, 注意, 这里一定要按照代值出现的顺序排列真正的值
最后再介绍一下buildClause这类函数
$db->update(pwQuery::buildClause("UPDATE :pw_table SET topic=topic+1 WHERE fid=:fid", array('pw_forumdata', $splitTopic['fid'])));
相信看过上面代值的解释后, 不难看懂这个函数的作用吧.
再来看看selectClause
Return $this->_db->get_one(pwQuery::selectClause('pw_kmd_info',
"tid=:tid", array($tid)));
与buildClause略有不同, 有3个参数, 第一个是表名, 后面两个组装成where语句
其他类似updateClause这些都跟selectClause大同小异.