📄 正在查看:twcms/kongphp/db/db_mysql.class.php
1<?php
2/**
3 * Copyright (C) 2013-2014 www.kongphp.com All rights reserved.
4 * Licensed http://www.gnu.org/licenses/lgpl.html
5 * Author: wuzhaohuan <kongphp@gmail.com>
6 */
7
8defined('KONG_PATH') || exit;
9class db_mysql implements db_interface {
10 private $conf;
11 public $tablepre; // 数据表前缀
12 //private $wlink; // 写(主)数据库
13 //private $rlink; // 读(从)数据库
14 //private $xlink; // 分发数据库
15
16 public function __construct(&$conf) {
17 $this->conf = &$conf;
18 $this->tablepre = $conf['master']['tablepre'];
19 }
20
21 /**
22 * 创建 MySQL 连接
23 * @param string $var 数据库链接名 只能是 wlink rlink xlink
24 * @return resource
25 */
26 public function __get($var) {
27 // 主数据库 (写)
28 if($var == 'wlink') {
29 $cfg = $this->conf['master'];
30 empty($cfg['engine']) && $cfg['engine'] = '';
31 $this->wlink = $this->connect($cfg['host'], $cfg['user'], $cfg['password'], $cfg['name'], $cfg['charset'], $cfg['engine']);
32 return $this->wlink;
33
34 // 从数据库群 (读)
35 }elseif($var == 'rlink') {
36 if(empty($this->conf['slaves'])) {
37 $this->rlink = $this->wlink;
38 return $this->rlink;
39 }
40 $n = rand(0, count($this->conf['slaves']) - 1);
41 $cfg = $this->conf['slaves'][$n];
42 empty($cfg['engine']) && $cfg['engine'] = '';
43 $this->rlink = $this->connect($cfg['host'], $cfg['user'], $cfg['password'], $cfg['name'], $cfg['charset'], $cfg['engine']);
44 return $this->rlink;
45
46 // 单点分发数据库 (负责所有表的 maxid count 读写)
47 }elseif($var == 'xlink') {
48 if(empty($this->conf['arbiter'])) {
49 $this->xlink = $this->wlink;
50 return $this->xlink;
51 }
52 $cfg = $this->conf['arbiter'];
53 empty($cfg['engine']) && $cfg['engine'] = '';
54 $this->xlink = $this->connect($cfg['host'], $cfg['user'], $cfg['password'], $cfg['name'], $cfg['charset'], $cfg['engine']);
55 return $this->xlink;
56 }
57 }
58
59 /**
60 * 读取一条数据
61 * @param string $key 键名 (高性能需求,键名必须使用索引字段)
62 * @return array
63 */
64 // string
65 // in: 'user-uid-2'
66 // out: array('uid'=>2, 'username'=>'two')
67 public function get($key) {
68 list($table, $keyarr, $keystr) = $this->key2arr($key);
69 $query = $this->query("SELECT * FROM {$this->tablepre}$table WHERE $keystr LIMIT 1", $this->rlink);
70 return mysql_fetch_assoc($query);
71 }
72
73 /**
74 * 读取多条数据
75 * @param array $keys 键名数组 (高性能需求,键名必须使用索引字段)
76 * @return array
77 */
78 // array
79 // in: array(
80 // 'article-cid-1-aid-1',
81 // 'article-cid-1-aid-2',
82 // )
83 // out: array(
84 // 'article-cid-1-aid-1'=>array('cid'=>1,'cid'=>1, 'title'=>'abc')
85 // 'article-cid-1-aid-2'=>array('cid'=>1,'cid'=>2, 'title'=>'bcd')
86 // )
87 public function multi_get($keys) {
88 // 下面这种方式读取比遍历读取效率高
89 $sql = '';
90 $ret = array();
91 foreach($keys as $k) {
92 $ret[$k] = array(); // 按原来的顺序赋值,避免后面的 OR 条件取出时顺序混乱
93 list($table, $keyarr, $keystr) = $this->key2arr($k);
94 $sql .= "$keystr OR ";
95 }
96 $sql = substr($sql, 0, -4);
97 if($sql) {
98 $query = $this->query("SELECT * FROM {$this->tablepre}$table WHERE $sql", $this->rlink);
99 while($row = mysql_fetch_assoc($query)) {
100 $keyname = $table;
101 foreach($keyarr as $k=>$v) {
102 $keyname .= "-$k-".$row[$k];
103 }
104 $ret[$keyname] = $row;
105 }
106 }
107 return $ret;
108 }
109
110 /**
111 * 写入一条数据 (包含了 insert 和 update)
112 * @param string $key 键名
113 * @param array $data 数据
114 * @return bool
115 */
116 public function set($key, $data) {
117 if(!is_array($data)) return FALSE;
118
119 list($table, $keyarr) = $this->key2arr($key);
120 $data += $keyarr;
121 $s = $this->arr2sql($data);
122
123 $exists = $this->get($key);
124 if(empty($exists)) {
125 return $this->query("INSERT INTO {$this->tablepre}$table SET $s", $this->wlink);
126 } else {
127 return $this->update($key, $data);
128 }
129 }
130
131 /**
132 * 更新一条数据 (相比 $this->set() 可以修改主键)
133 * @param string $key 键名
134 * @param array $data 数据
135 * @return bool
136 */
137 public function update($key, $data) {
138 list($table, $keyarr, $keystr) = $this->key2arr($key);
139 $s = $this->arr2sql($data);
140 return $this->query("UPDATE {$this->tablepre}$table SET $s WHERE $keystr LIMIT 1", $this->wlink);
141 }
142
143 /**
144 * 删除一条数据
145 * @param string $key 键名
146 * @return bool
147 */
148 public function delete($key) {
149 list($table, $keyarr, $keystr) = $this->key2arr($key);
150 return $this->query("DELETE FROM {$this->tablepre}$table WHERE $keystr LIMIT 1", $this->wlink);
151 }
152
153 /**
154 * 读取/设置 表最大ID
155 * @param string $key 键名 只能是表名+一个字段 如:'user-uid'(uid为自增字段)
156 * @param boot/int $val 设置值 有三种情况 1.不填为读取(默认) 2.基础上增加 如:'+1' 3.设置指定值
157 * @return int
158 */
159 // maxid('user-uid') 读取 user 表最大 uid
160 // maxid('user-uid', '+1') 设置 maxid + 1, 用于占位,保证 key 不会重复
161 // maxid('user-uid', 10000) 设置 maxid 为 10000
162 public function maxid($key, $val = FALSE) {
163 list($table, $col) = explode('-', $key);
164 $maxid = $this->table_maxid($key);
165 if($val === FALSE) {
166 return $maxid;
167 }elseif(is_string($val)) {
168 $val = max(0, $maxid + intval($val));
169 }
170 $this->query("UPDATE {$this->tablepre}framework_maxid SET maxid='$val' WHERE name='$table' LIMIT 1", $this->xlink);
171 return $val;
172 }
173
174 /**
175 * 读取表最大ID (如果不存在自动创建表和设置最大ID)
176 * @param string $key 键名 只能是表名+一个字段 如:'user-uid'(uid一般为主键)
177 * @return int
178 */
179 public function table_maxid($key) {
180 list($table, $col) = explode('-', $key);
181
182 $maxid = FALSE;
183 $query = $this->query("SELECT maxid FROM {$this->tablepre}framework_maxid WHERE name='$table' LIMIT 1", $this->xlink, FALSE);
184
185 if($query) {
186 $maxid = $this->result($query, 0);
187 }elseif(mysql_errno($this->xlink) == 1146) {
188 $sql = "CREATE TABLE `{$this->tablepre}framework_maxid` (";
189 $sql .= "`name` char(32) NOT NULL default '',";
190 $sql .= "`maxid` int(10) unsigned NOT NULL default '0',";
191 $sql .= "PRIMARY KEY (`name`)";
192 $sql .= ") ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci";
193 $this->query($sql, $this->xlink);
194 }else{
195 throw new Exception('framework_maxid error, mysql_error:'.mysql_error());
196 }
197 if($maxid === FALSE) {
198 $query = $this->query("SELECT MAX($col) FROM {$this->tablepre}$table", $this->wlink);
199 $maxid = $this->result($query, 0);
200 $this->query("INSERT INTO {$this->tablepre}framework_maxid SET name='$table', maxid='$maxid'", $this->xlink);
201 }
202 return $maxid;
203 }
204
205 /**
206 * 读取/设置 表的总行数
207 * @param string $table 表名
208 * @param boot/int $val 设置值 有四种情况 1.不填为读取(默认) 2.基础上增加 如:'+1' 3.基础上减少 如:'-1' 4.设置指定值
209 * @return int
210 */
211 public function count($table, $val = FALSE) {
212 $count = $this->table_count($table);
213 if($val === FALSE) {
214 return $count;
215 }elseif(is_string($val)) {
216 if($val[0] == '+') {
217 $val = $count + intval($val);
218 }elseif($val[0] == '-') {
219 $val = max(0, $count + intval($val));
220 }
221 }
222 $this->query("UPDATE {$this->tablepre}framework_count SET count='$val' WHERE name='$table' LIMIT 1", $this->xlink);
223 return $val;
224 }
225
226 /**
227 * 读取表的总行数 (如果不存在自动创建表和设置总行数)
228 * @param string $table 表名
229 * @return int
230 */
231 public function table_count($table) {
232 $count = FALSE;
233 $query = $this->query("SELECT count FROM {$this->tablepre}framework_count WHERE name='$table' LIMIT 1", $this->xlink, FALSE);
234
235 if($query) {
236 $count = $this->result($query, 0);
237 }elseif(mysql_errno($this->xlink) == 1146) {
238 $sql = "CREATE TABLE {$this->tablepre}framework_count (";
239 $sql .= "`name` char(32) NOT NULL default '',";
240 $sql .= "`count` int(10) unsigned NOT NULL default '0',";
241 $sql .= "PRIMARY KEY (`name`)";
242 $sql .= ") ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci";
243 $this->query($sql, $this->xlink);
244 }else{
245 throw new Exception('framework_cout error, mysql_error:'.mysql_error());
246 }
247 if($count === FALSE) {
248 $query = $this->query("SELECT COUNT(*) FROM {$this->tablepre}$table", $this->wlink);
249 $count = $this->result($query, 0);
250 $this->query("INSERT INTO {$this->tablepre}framework_count SET name='$table', count='$count'", $this->xlink);
251 }
252 return $count;
253 }
254
255 /**
256 * 清空表
257 * @param string $table 表名 (表不存在会报错,但无关紧要)
258 * @return int
259 */
260 public function truncate($table) {
261 try {
262 $this->query("TRUNCATE {$this->tablepre}$table");
263 return TRUE;
264 } catch(Exception $e) {
265 return FALSE;
266 }
267 }
268
269 /**
270 * 根据条件读取数据 (返回数组)
271 * @param string $table 表名
272 * @param array $pri 主键
273 * @param array $where 条件
274 * @param array $order 排序
275 * @param int $start 开始位置
276 * @param int $limit 读取几条
277 * @return array
278 */
279 // in:
280 // find_fetch('user', 'uid', array('uid'=> 100), array('uid'=>1), 0, 10);
281 // find_fetch('user', 'uid', array('uid'=> array('>'=>'100', '<'=>'200')), array('uid'=>1), 0, 10);
282 // find_fetch('user', 'uid', array('username'=> array('LIKE'=>'abc'), array('uid'=>1), 0, 10);
283
284 // out:
285 // array(
286 // 'user-uid-1'=>array('uid'=>1, 'username'=>'zhangsan'),
287 // 'user-uid-2'=>array('uid'=>2, 'username'=>'lisi'),
288 // 'user-uid-3'=>array('uid'=>3, 'username'=>'wangwu'),
289 // )
290 public function find_fetch($table, $pri, $where = array(), $order = array(), $start = 0, $limit = 0) {
291 $key_arr = $this->find_fetch_key($table, $pri, $where, $order, $start, $limit);
292 if(empty($key_arr)) return array();
293 return $this->multi_get($key_arr);
294 }
295
296 /**
297 * 根据条件返回 key 数组
298 * @param string $table 表名
299 * @param array $pri 主键
300 * @param array $where 条件
301 * @param array $order 排序
302 * @param int $start 开始位置
303 * @param int $limit 读取几条
304 * @return array
305 */
306 // out:
307 // array (
308 // 'user-uid-1',
309 // 'user-uid-2',
310 // 'user-uid-3',
311 // )
312 public function find_fetch_key($table, $pri, $where = array(), $order = array(), $start = 0, $limit = 0) {
313 $pris = implode(',', $pri);
314 $s = "SELECT $pris FROM {$this->tablepre}$table";
315 $s .= $this->arr2where($where);
316 if(!empty($order)) {
317 $s .= ' ORDER BY ';
318 $comma = '';
319 foreach($order as $k=>$v) {
320 $s .= $comma."$k ".($v == 1 ? ' ASC ' : ' DESC ');
321 $comma = ',';
322 }
323 }
324 $s .= ($limit ? " LIMIT $start,$limit" : '');
325
326 $ret = array();
327 $query = $this->query($s, $this->rlink);
328 while($row = mysql_fetch_assoc($query)) {
329 $keystr = '';
330 foreach($pri as $k) {
331 $keystr .= "-$k-".$row[$k];
332 }
333 $ret[] = $table.$keystr;
334 }
335 return $ret;
336 }
337
338 /**
339 * 根据条件批量更新数据
340 * @param string $table 表名
341 * @param array $where 条件
342 * @param array $lowprority 是否开启不锁定表
343 * @return int 返回影响的记录行数
344 */
345 public function find_update($table, $where, $data, $lowprority = FALSE) {
346 $where = $this->arr2where($where);
347 $data = $this->arr2sql($data);
348 $lpy = $lowprority ? 'LOW_PRIORITY' : '';
349 $this->query("UPDATE $lpy {$this->tablepre}$table SET $data $where", $this->wlink);
350 return mysql_affected_rows($this->wlink);
351 }
352
353 /**
354 * 根据条件批量删除数据
355 * @param string $table 表名
356 * @param array $where 条件
357 * @param array $lowprority 是否开启不锁定表
358 * @return int 返回影响的记录行数
359 */
360 public function find_delete($table, $where, $lowprority = FALSE) {
361 $where = $this->arr2where($where);
362 $lpy = $lowprority ? 'LOW_PRIORITY' : '';
363 $this->query("DELETE $lpy FROM {$this->tablepre}$table $where", $this->wlink);
364 return mysql_affected_rows($this->wlink);
365 }
366
367 /**
368 * 准确获取最大ID
369 * @param string $key 键名
370 * @return int
371 */
372 public function find_maxid($key) {
373 list($table, $maxid) = explode('-', $key);
374 $arr = $this->fetch_first("SELECT MAX($maxid) AS num FROM {$this->tablepre}$table");
375 return isset($arr['num']) ? intval($arr['num']) : 0;
376 }
377
378 /**
379 * 准确获取总条数
380 * @param string $table 表名
381 * @param array $where 条件
382 * @return int
383 */
384 public function find_count($table, $where = array()) {
385 $where = $this->arr2where($where);
386 $arr = $this->fetch_first("SELECT COUNT(*) AS num FROM {$this->tablepre}$table $where");
387 return isset($arr['num']) ? intval($arr['num']) : 0;
388 }
389
390 /**
391 * 创建索引
392 * @param string $table 表名
393 * @param array $index 键名数组 // array('uid'=>1, 'dateline'=>-1, 'unique'=>TRUE, 'dropDups'=>TRUE) 为了配合 mongodb 的索引才这样设计的
394 * @return boot
395 */
396 public function index_create($table, $index) {
397 $keys = implode(',', array_keys($index));
398 $keyname = implode('_', array_keys($index));
399 return $this->query("ALTER TABLE {$this->tablepre}$table ADD INDEX $keyname($keys)", $this->wlink);
400 }
401
402 /**
403 * 删除索引
404 * @param string $table 表名
405 * @param array $index 键名数组
406 * @return boot
407 */
408 public function index_drop($table, $index) {
409 $keys = implode(',', array_keys($index));
410 $keyname = implode('_', array_keys($index));
411 return $this->query("ALTER TABLE {$this->tablepre}$table DROP INDEX $keyname", $this->wlink);
412 }
413
414 // +------------------------------------------------------------------------------
415 // | 以下是公共方法,但不推荐外部使用
416 // +------------------------------------------------------------------------------
417 /**
418 * 连接 MySQL 服务器
419 * @param string $host 主机
420 * @param string $user 用户名
421 * @param string $pass 密码
422 * @param string $name 数据库名称
423 * @param string $charset 字符集
424 * @param string $engine 数据库引擎
425 * @return resource
426 */
427 public function connect($host, $user, $pass, $name, $charset = 'utf8', $engine = '') {
428 $link = mysql_connect($host, $user, $pass);
429 if(!$link) {
430 throw new Exception(mysql_error());
431 }
432 $result = mysql_select_db($name, $link);
433 if(!$result) {
434 throw new Exception(mysql_error());
435 }
436 if(!empty($engine) && $engine == 'InnoDB') {
437 $this->query("SET innodb_flush_log_at_trx_commit=no", $link);
438 }
439
440 // 不考虑 mysql 5.0.1 下以版本
441 $this->query("SET character_set_connection=$charset, character_set_results=$charset, character_set_client=binary, sql_mode=''", $link);
442 //$this->query("SET names utf8, sql_mode=''", $link);
443 return $link;
444 }
445
446 /**
447 * 发送一条 MySQL 查询
448 * @param string $sql SQL 语句
449 * @param string $link 打开的连接
450 * @param boot $isthrow 错误时是否抛
451 * @return resource
452 */
453 public function query($sql, $link = NULL, $isthrow = TRUE) {
454 empty($link) && $link = $this->wlink;
455
456 if(defined('DEBUG') && DEBUG && isset($_ENV['_sqls']) && count($_ENV['_sqls']) < 1000) {
457 $start = microtime(1);
458 $result = mysql_query($sql, $link);
459 $runtime = number_format(microtime(1) - $start, 4);
460
461 // explain 分析 select 语句
462 $explain_str = '';
463 if(substr($sql, 0, 6) == 'SELECT') {
464 $query = mysql_query("explain $sql", $link);
465 if($query !== FALSE) {
466 $explain_arr = mysql_fetch_assoc($query);
467 //print_r($explain_arr);
468 $explain_str = ' <font color="blue">[explain type: '.$explain_arr['type'].' | rows: '.$explain_arr['rows'].']</font>';
469 }
470 }
471 $_ENV['_sqls'][] = ' <font color="red">[time:'.$runtime.'s]</font> '.htmlspecialchars(stripslashes($sql)).$explain_str;
472 }else{
473 $result = mysql_query($sql, $link);
474 }
475
476 if(!$result && $isthrow) {
477 $s = 'MySQL Query Error: <b>'.$sql.'</b>. '.mysql_error();
478
479 if(defined('DEBUG') && !DEBUG) $s = str_replace($this->tablepre, '***', $s); // 防止泄露敏感信息
480
481 throw new Exception($s);
482 }
483 $_ENV['_sqlnum']++;
484 return $result;
485 }
486
487 /**
488 * 获取第一条数据
489 * @param string $sql SQL 语句
490 * @param string $link 打开的连接
491 * @return array
492 */
493 public function fetch_first($sql, $link = NULL) {
494 empty($link) && $link = $this->rlink;
495 $query = $this->query($sql, $link);
496 return mysql_fetch_assoc($query);
497 }
498
499 /**
500 * 获取多条数据 (特殊情况会用到)
501 * @param string $sql SQL 语句
502 * @param string $link 打开的连接
503 * @return array
504 */
505 public function fetch_all($sql, $link = NULL) {
506 empty($link) && $link = $this->rlink;
507 $query = $this->query($sql, $link);
508 $ret = array();
509 while($row = mysql_fetch_assoc($query)) {
510 $ret[] = $row;
511 }
512 return $ret;
513 }
514
515 /**
516 * 获取结果数据
517 * @param resource $query 查询结果集
518 * @param int $row 第几列
519 * @return int
520 */
521 public function result($query, $row) {
522 return mysql_num_rows($query) ? intval(mysql_result($query, $row)) : FALSE;
523 }
524
525 /**
526 * 获取 mysql 版本
527 * @return string
528 */
529 public function version() {
530 return mysql_get_server_info($this->rlink);
531 }
532
533 /**
534 * 关闭读写数据库连接
535 */
536 public function __destruct() {
537 if(!empty($this->wlink)) {
538 mysql_close($this->wlink);
539 }
540 if(!empty($this->rlink) && !empty($this->wlink) && $this->rlink != $this->wlink) {
541 mysql_close($this->rlink);
542 }
543 }
544
545 /**
546 * 将数组转换为 where 语句
547 * @param array $arr 数组
548 * @return string
549 * in: array('id'=> array('>'=>'10', '<'=>'200'))
550 * out: WHERE id>'10' AND id<'200'
551 * 支持: '>=', '<=', '>', '<', 'LIKE', 'IN' (尽量少用,能不用则不用。'LIKE' 会导致全表扫描,大数据时不要使用)
552 * 注意1: 为考虑多种数据库兼容和性能问题,其他表达式不要使用,如:!= 会导致全表扫描
553 * 注意2: 高性能准则要让SQL走索引,保证查询至少达到range级别
554 */
555 private function arr2where($arr) {
556 $s = '';
557 if(!empty($arr)) {
558 foreach($arr as $key=>$val) {
559 if(is_array($val)) {
560 foreach($val as $k=>$v) {
561 if(is_array($v)) {
562 if($k === 'IN' && $v) {
563 foreach($v as $i) {
564 $i = addslashes($i);
565 $s .= "$key='$i' OR "; // 走索引时,OR 比 IN 快
566 }
567 $s = substr($s, 0, -4).' AND ';
568 }
569 }else{
570 $v = addslashes($v);
571 if($k === 'LIKE') {
572 $s .= "$key LIKE '%$v%' AND ";
573 }else{
574 $s .= "$key$k'$v' AND ";
575 }
576 }
577 }
578 }else{
579 $val = addslashes($val);
580 $s .= "$key='$val' AND ";
581 }
582 }
583 $s && $s = ' WHERE '.substr($s, 0, -5);
584 }
585 return $s;
586 }
587
588 /**
589 * 将数组转换为SQL语句
590 * @param array $arr 数组
591 * @return string
592 * in: array('cid'=>1, 'aid'=>2)
593 * out: cid='1',aid='2'
594 */
595 private function arr2sql($arr) {
596 $s = '';
597 foreach($arr as $k=>$v) {
598 $v = addslashes($v);
599 $s .= "$k='$v',";
600 }
601 return rtrim($s, ',');
602 }
603
604 /**
605 * 将键名转换为数组
606 * @param string $key 键名
607 * @return array
608 * in: article-cid-1-aid-2
609 * out: array('article', array('cid'=>1, 'aid'=>2), 'cid=1 AND aid=2')
610 */
611 private function key2arr($key) {
612 $arr = explode('-', $key);
613
614 if(empty($arr[0])) {
615 throw new Exception('table name is empty.');
616 }
617
618 $table = $arr[0];
619 $keyarr = array();
620 $keystr = '';
621 $len = count($arr);
622 for($i = 1; $i < $len; $i = $i + 2) {
623 if(isset($arr[$i + 1])) {
624 $v = $arr[$i + 1];
625 $keyarr[$arr[$i]] = is_numeric($v) ? intval($v) : $v; // 因为 mongodb 区分数字和字符串
626
627 $keystr .= ($keystr ? ' AND ' : '').$arr[$i]."='".addslashes($v)."'";
628 } else {
629 $keyarr[$arr[$i]] = NULL;
630 }
631 }
632
633 if(empty($keystr)) {
634 throw new Exception('keystr name is empty.');
635 }
636 return array($table, $keyarr, $keystr);
637 }
638}
639?>
640