数据查询(CRUD

数据查询描述软件系统中数据库或者持久层的基本操作功能, 主要包括增加(Create)、查询(Retrieve)、更新(Update)和删除(Delete), 统称为CRUD.

MySQL是一个小型关系型数据库管理系统,由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,许多中小型网站选择了MySQL作为网站数据库。在phpwind论坛系统中.

Phpwind论坛系统采用的就是MYSQL作为网站数据库, 并且将mysql的查询语言封装, 使原本繁杂的操作语言变得简单. 封装的类分为两种, 分别在require/db_mysql.phprequire/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_querymysql_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里面queryupdate方法的定义了:

    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) . ')');

}

上面的代码作用是批量删除附件.

 

 

上面系统地讲述了CRUDphpwind代码系统中的实现, 可能大家在看的过程中经常碰到那么不是很明白作用的函数, 比如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, 这个函数是用来通过","字符连接数组转换的字符, 比如我们想查找tid1, 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大同小异.