浅谈DISCUZ X系列 数据库的操作

还记的曾经写DISCUZ插件时,凡是要用到数据库的时候,必然会写一大堆的数据库处理语句。只有一个数据库的操作类 dbstuff,我们只能用到原始的操作方法query,fetch_array等去检索数据库。写数据库语句是一件头疼的事情。


而在X系列中,DISCUZ是如何操作数据库的呢?


我们打开一个文件,这里我打开index.php,会发现类似这个样子的代码:

    C::t('common_domain')->fetch_by_domain_domainroot($_ENV['prefixdomain'], $_ENV['domainroot']);


这就是DISCUZ X系列的数据库操作了,完全不同于7.0以及以前的操作方式,那么它究竟是如何执行的呢?

按照从左到有的顺序,我们一步一步的拆分,首先是C,这个类在我之前的文章中提到过,他是core class的继承类,也可以认为就是core 类,我们看到他有一个静态的方法。

        public static function t($name) {
            return self::_make_obj($name, 'table', DISCUZ_TABLE_EXTENDABLE);
        }


在这个方法中,又调用了_make_obj方法,继续跟进:

        protected static function _make_obj($name, $type, $extendable = false, $p = array()) {
            $pluginid = null;
            if($name[0] === '#') {
                list(, $pluginid, $name) = explode('#', $name);
            }
            $cname = $type.'_'.$name;
            if(!isset(self::$_tables[$cname])) {
                if(!class_exists($cname, false)) {
                    self::import(($pluginid ? 'plugin/'.$pluginid : 'class').'/'.$type.'/'.$name);
                }
                if($extendable) {
                    self::$_tables[$cname] = new discuz_container();
                    switch (count($p)) {
                        case 0:    self::$_tables[$cname]->obj = new $cname();break;
                        case 1:    self::$_tables[$cname]->obj = new $cname($p[1]);break;
                        case 2:    self::$_tables[$cname]->obj = new $cname($p[1], $p[2]);break;
                        case 3:    self::$_tables[$cname]->obj = new $cname($p[1], $p[2], $p[3]);break;
                        case 4:    self::$_tables[$cname]->obj = new $cname($p[1], $p[2], $p[3], $p[4]);break;
                        case 5:    self::$_tables[$cname]->obj = new $cname($p[1], $p[2], $p[3], $p[4], $p[5]);break;
                        default: $ref = new ReflectionClass($cname);self::$_tables[$cname]->obj = $ref->newInstanceArgs($p);unset($ref);break;
                    }
                } else {
                    self::$_tables[$cname] = new $cname();
                }
            }
            return self::$_tables[$cname];
        }


这是一个protected静态方法,首先方法设置$pluginid为NULL,接着判断$name[0] === '#',(注意:===恒等计算符,和比较运算符号“==”的区别是 “==”不会检查条件式的表达式的类型,恒等计算符会同时检查表达式的值与类型。比如 ( 1==true ) 为true 但是 (1 === true) 就是false了,因为1是整形,true为bool型)

这里$name 是common_domain,那么$name[1]就是‘c’,得到cname,table_common_domain,判断是否存在self::$_table[table_common_domain](这里就是为了防止重复加载),第一次执行,可定不存在,执行if(!class_exists(table_common_domain,false)),判断class是否不存在,然后取反,得到false,继续执行。

到这里if($extendable) ,$extendable默认为false(这里的$extendable大概为扩展表的意思吧),因此执行self::$_tables[$cname] = new $cname();  创建table_common_domain的类,程序会调用_autoload(不懂的autoload的同学,可以看看我的上一篇文章  (菜鸟篇)从Discuz X系列中学PHP core )去加载 \source\class\table目录下的table_common_domain.php文件。

来到了table_common_domain类,这个类继承自discuz_table  class table_common_domain extends discuz_table   我们再去打开 source\class\discuz\discuz_table.php,又发现了

discuz_table原来是从discuz_base类继承下来的,那么discuz_base类又写了什么呢?

打开source\class\discuz\discuz_base.php


    abstract class discuz_base
    {
        private $_e;
        private $_m;
     
        public function __construct() {
     
        }
     
        public function __set($name, $value) {
            $setter='set'.$name;
            if(method_exists($this,$setter)) {
                return $this->$setter($value);
            } elseif($this->canGetProperty($name)) {
                throw new Exception('The property "'.get_class($this).'->'.$name.'" is readonly');
            } else {
                throw new Exception('The property "'.get_class($this).'->'.$name.'" is not defined');
            }
        }
     
        public function __get($name) {
            $getter='get'.$name;
            if(method_exists($this,$getter)) {
                return $this->$getter();
            } else {
                throw new Exception('The property "'.get_class($this).'->'.$name.'" is not defined');
            }
        }
     
        public function __call($name,$parameters) {
            throw new Exception('Class "'.get_class($this).'" does not have a method named "'.$name.'".');
        }
     
        public function canGetProperty($name)
        {
            return method_exists($this,'get'.$name);
        }
     
        public function canSetProperty($name)
        {
            return method_exists($this,'set'.$name);
        }
     
        public function __toString() {
            return get_class($this);
        }
     
        public function __invoke() {
            return get_class($this);
        }
     
    }
    ?>


discuz_base原来是一个抽象类,这个类也是discuz的基类,设置了魔术方法____construct、__set、__get、__call、__tostring、__invoke,以及判断是否存在get,set方法的函数。


__construct 构造方法,没有具体内容,悄悄飘过。

__set方法,这个方法,当我们调用类的setXXXXXX方法的时候会触发,同样__get方法也是当我们调用getXXXXXX方法的时候会触发,注意是在get,set之前触发。这里是判断调用的setXXXXXX方法,或者getXXXXXX方法是否存在。__call方法会在调用该类的方法前调用,同样也是判断方法是否存在,若不存在 抛出异常。在__set,__get中变脸$name就是set或者get之后跟的字符串,而__call中,直接就是调用的方法名称。__tostring,过去该类的字符串描述,学java的同学经常使用,不多解释。而当尝试以调用函数的方式调用一个对象时,__invoke方法会被自动调用。比如 $a = new mysql(); $a()这样的方式就会触发__invoke函数。

get_class会返回当前调用该方法的类名,是子类调用的话,返回的就是子类的名称。


再回到discuz_table类:

首先看它的属性:

        public $data = array();
     
        public $methods = array();
     
        protected $_table;
        protected $_pk;
        protected $_pre_cache_key;
        protected $_cache_ttl;
        protected $_allowmem;


从上到下分别是数据,方法,表,键名,缓存,以及是否允许缓存系统。


再返回看table_common_domain类,这下终于回到了这个”孙子“类了,哎,看类就是头大,一会儿就绕晕了,说不定哪个变量就在父类或者子类中。

首先会调用table_common_domain类的构造函数

    public function __construct() {
     
            $this->_table = 'common_domain';
            $this->_pk    = '';
     
            parent::__construct();
        }

 

这里两个赋值,然后调用父类的构造函数:discuz_table的构造函数

        public function __construct($para = array()) {
            if(!empty($para)) {
                $this->_table = $para['table'];
                $this->_pk = $para['pk'];
            }
            if(isset($this->_pre_cache_key) && (($ttl = getglobal('setting/memory/'.$this->_table)) !== null || ($ttl = $this->_cache_ttl) !== null) && memory('check')) {
                $this->_cache_ttl = $ttl;
                $this->_allowmem = true;
            }
            $this->_init_extend();
            parent::__construct();
        }


上边是基本的赋值,我们暂时不考虑,主要就是缓存极致了,主要看最后两句,$this->_init_extend();  与 parent::__construct();,前者初始化了扩展信息,后者调用父类构造函数,在当前文件中_init_extend方法为空,但是不要以为就什么都不做了,因为在其子类中init_extend可能会被覆盖,所以有可能会调用到子类的这个方法。这里我们在写扩展表的时候 ,就可以把相关的初始化操作写到这个方法里边。


构造函数完毕,回到最上边的语句,该执行红色字体部分了:

C::t('common_domain')->fetch_by_domain_domainroot($_ENV['prefixdomain'], $_ENV['domainroot']);

下边这个方法:

        public function fetch_by_domain_domainroot($domain, $droot) {
            return DB::fetch_first('SELECT * FROM %t WHERE domain=%s AND domainroot=%s', array($this->_table, $domain, $droot));
        }


我们又看到了一个新的类DB,其实也不算新类,这个类在core类中提到过,class_core.php文件中:class DB extends discuz_database {}

可以看到这个类是discuz_database类的一个子类,继续去看discuz_database

source\class\discuz\discuz_database.php

    class discuz_database {
     
        public static $db;
     
        public static $driver;
     
        public static function init($driver, $config) {
            self::$driver = $driver;
            self::$db = new $driver;
            self::$db->set_config($config);
            self::$db->connect();
        }
     
        public static function object() {
            return self::$db;
        }
     
        public static function table($table) {
            return self::$db->table_name($table);
        }
     
        public static function delete($table, $condition, $limit = 0, $unbuffered = true) {
            if (empty($condition)) {
                return false;
            } elseif (is_array($condition)) {
                if (count($condition) == 2 && isset($condition['where']) && isset($condition['arg'])) {
                    $where = self::format($condition['where'], $condition['arg']);
                } else {
                    $where = self::implode_field_value($condition, ' AND ');
                }
            } else {
                $where = $condition;
            }
            $limit = dintval($limit);
            $sql = "DELETE FROM " . self::table($table) . " WHERE $where " . ($limit > 0 ? "LIMIT $limit" : '');
            return self::query($sql, ($unbuffered ? 'UNBUFFERED' : ''));
        }
     
        public static function insert($table, $data, $return_insert_id = false, $replace = false, $silent = false) {
     
            $sql = self::implode($data);
     
            $cmd = $replace ? 'REPLACE INTO' : 'INSERT INTO';
     
            $table = self::table($table);
            $silent = $silent ? 'SILENT' : '';
     
            return self::query("$cmd $table SET $sql", null, $silent, !$return_insert_id);
        }
     
        public static function update($table, $data, $condition, $unbuffered = false, $low_priority = false) {
            $sql = self::implode($data);
            if(empty($sql)) {
                return false;
            }
            $cmd = "UPDATE " . ($low_priority ? 'LOW_PRIORITY' : '');
            $table = self::table($table);
            $where = '';
            if (empty($condition)) {
                $where = '1';
            } elseif (is_array($condition)) {
                $where = self::implode($condition, ' AND ');
            } else {
                $where = $condition;
            }
            $res = self::query("$cmd $table SET $sql WHERE $where", $unbuffered ? 'UNBUFFERED' : '');
            return $res;
        }
     
        public static function insert_id() {
            return self::$db->insert_id();
        }
     
        public static function fetch($resourceid, $type = 'MYSQL_ASSOC') {
            return self::$db->fetch_array($resourceid, $type);
        }
     
        public static function fetch_first($sql, $arg = array(), $silent = false) {
            $res = self::query($sql, $arg, $silent, false);
            $ret = self::$db->fetch_array($res);
            self::$db->free_result($res);
            return $ret ? $ret : array();
        }
     
        public static function fetch_all($sql, $arg = array(), $keyfield = '', $silent=false) {
     
            $data = array();
            $query = self::query($sql, $arg, $silent, false);
            while ($row = self::$db->fetch_array($query)) {
                if ($keyfield && isset($row[$keyfield])) {
                    $data[$row[$keyfield]] = $row;
                } else {
                    $data[] = $row;
                }
            }
            self::$db->free_result($query);
            return $data;
        }
     
        public static function result($resourceid, $row = 0) {
            return self::$db->result($resourceid, $row);
        }
     
        public static function result_first($sql, $arg = array(), $silent = false) {
            $res = self::query($sql, $arg, $silent, false);
            $ret = self::$db->result($res, 0);
            self::$db->free_result($res);
            return $ret;
        }
     
        public static function query($sql, $arg = array(), $silent = false, $unbuffered = false) {
            if (!empty($arg)) {
                if (is_array($arg)) {
                    $sql = self::format($sql, $arg);
                } elseif ($arg === 'SILENT') {
                    $silent = true;
     
                } elseif ($arg === 'UNBUFFERED') {
                    $unbuffered = true;
                }
            }
            self::checkquery($sql);
     
            $ret = self::$db->query($sql, $silent, $unbuffered);
            if (!$unbuffered && $ret) {
                $cmd = trim(strtoupper(substr($sql, 0, strpos($sql, ' '))));
                if ($cmd === 'SELECT') {
     
                } elseif ($cmd === 'UPDATE' || $cmd === 'DELETE') {
                    $ret = self::$db->affected_rows();
                } elseif ($cmd === 'INSERT') {
                    $ret = self::$db->insert_id();
                }
            }
            return $ret;
        }
     
        public static function num_rows($resourceid) {
            return self::$db->num_rows($resourceid);
        }
     
        public static function affected_rows() {
            return self::$db->affected_rows();
        }
     
        public static function free_result($query) {
            return self::$db->free_result($query);
        }
     
        public static function error() {
            return self::$db->error();
        }
     
        public static function errno() {
            return self::$db->errno();
        }
     
        public static function checkquery($sql) {
            return discuz_database_safecheck::checkquery($sql);
        }
     
        public static function quote($str, $noarray = false) {
     
            if (is_string($str))
                return '\'' . addcslashes($str, "\n\r\\'\"\032") . '\'';
     
            if (is_int($str) or is_float($str))
                return '\'' . $str . '\'';
     
            if (is_array($str)) {
                if($noarray === false) {
                    foreach ($str as &$v) {
                        $v = self::quote($v, true);
                    }
                    return $str;
                } else {
                    return '\'\'';
                }
            }
     
            if (is_bool($str))
                return $str ? '1' : '0';
     
            return '\'\'';
        }
     
        public static function quote_field($field) {
            if (is_array($field)) {
                foreach ($field as $k => $v) {
                    $field[$k] = self::quote_field($v);
                }
            } else {
                if (strpos($field, '`') !== false)
                    $field = str_replace('`', '', $field);
                $field = '`' . $field . '`';
            }
            return $field;
        }
     
        public static function limit($start, $limit = 0) {
            $limit = intval($limit > 0 ? $limit : 0);
            $start = intval($start > 0 ? $start : 0);
            if ($start > 0 && $limit > 0) {
                return " LIMIT $start, $limit";
            } elseif ($limit) {
                return " LIMIT $limit";
            } elseif ($start) {
                return " LIMIT $start";
            } else {
                return '';
            }
        }
     
        public static function order($field, $order = 'ASC') {
            if(empty($field)) {
                return '';
            }
            $order = strtoupper($order) == 'ASC' || empty($order) ? 'ASC' : 'DESC';
            return self::quote_field($field) . ' ' . $order;
        }
     
        public static function field($field, $val, $glue = '=') {
     
            $field = self::quote_field($field);
     
            if (is_array($val)) {
                $glue = $glue == 'notin' ? 'notin' : 'in';
            } elseif ($glue == 'in') {
                $glue = '=';
            }
     
            switch ($glue) {
                case '=':
                    return $field . $glue . self::quote($val);
                    break;
                case '-':
                case '+':
                    return $field . '=' . $field . $glue . self::quote((string) $val);
                    break;
                case '|':
                case '&':
                case '^':
                    return $field . '=' . $field . $glue . self::quote($val);
                    break;
                case '>':
                case '<':
                case '<>':
                case '<=':
                case '>=':
                    return $field . $glue . self::quote($val);
                    break;
     
                case 'like':
                    return $field . ' LIKE(' . self::quote($val) . ')';
                    break;
     
                case 'in':
                case 'notin':
                    $val = $val ? implode(',', self::quote($val)) : '\'\'';
                    return $field . ($glue == 'notin' ? ' NOT' : '') . ' IN(' . $val . ')';
                    break;
     
                default:
                    throw new DbException('Not allow this glue between field and value: "' . $glue . '"');
            }
        }
     
        public static function implode($array, $glue = ',') {
            $sql = $comma = '';
            $glue = ' ' . trim($glue) . ' ';
            foreach ($array as $k => $v) {
                $sql .= $comma . self::quote_field($k) . '=' . self::quote($v);
                $comma = $glue;
            }
            return $sql;
        }
     
        public static function implode_field_value($array, $glue = ',') {
            return self::implode($array, $glue);
        }
     
        public static function format($sql, $arg) {
            $count = substr_count($sql, '%');
            if (!$count) {
                return $sql;
            } elseif ($count > count($arg)) {
                throw new DbException('SQL string format error! This SQL need "' . $count . '" vars to replace into.', 0, $sql);
            }
     
            $len = strlen($sql);
            $i = $find = 0;
            $ret = '';
            while ($i <= $len && $find < $count) {
                if ($sql{$i} == '%') {
                    $next = $sql{$i + 1};
                    if ($next == 't') {
                        $ret .= self::table($arg[$find]);
                    } elseif ($next == 's') {
                        $ret .= self::quote(is_array($arg[$find]) ? serialize($arg[$find]) : (string) $arg[$find]);
                    } elseif ($next == 'f') {
                        $ret .= sprintf('%F', $arg[$find]);
                    } elseif ($next == 'd') {
                        $ret .= dintval($arg[$find]);
                    } elseif ($next == 'i') {
                        $ret .= $arg[$find];
                    } elseif ($next == 'n') {
                        if (!empty($arg[$find])) {
                            $ret .= is_array($arg[$find]) ? implode(',', self::quote($arg[$find])) : self::quote($arg[$find]);
                        } else {
                            $ret .= '0';
                        }
                    } else {
                        $ret .= self::quote($arg[$find]);
                    }
                    $i++;
                    $find++;
                } else {
                    $ret .= $sql{$i};
                }
                $i++;
            }
            if ($i < $len) {
                $ret .= substr($sql, $i);
            }
            return $ret;
        }
     
    }


在这个类中,我们终于看到了熟悉的变量 $db,对了,这个变量就是我们在discuz 7.X时代经常使用的dbstuff类。

同时找到DB::fetch_first 方法,

        public static function fetch_first($sql, $arg = array(), $silent = false) {
            $res = self::query($sql, $arg, $silent, false);
            $ret = self::$db->fetch_array($res);
            self::$db->free_result($res);
            return $ret ? $ret : array();
        }


看到这里我们也大概能整理出一点思路:

Discuz X中 使用discuz_database类将以前的dbstuff类进行了新的封装,使其变成已经完全由静态方法操作数据库的类,并且添加了一些新的功能,比如SQL安全检查。该类执行基础的SQL语句,

然后再discuz_table类中有进行了第二次包装,使得discuz_table具有了一些简单的操作,比如updata,delete,比以前操作更简单,不用写语句,直接传入数组,以数组键名作为数据库键名,在函数内部重组SQL语句进行操作,大大简化了程序员二次开发的工作量,并且更加安全。

若是基本的操作不够用,那么discuz_table类进行继承,得到一个新类,在其中扩展该类,使其拥有自定义的更加复杂的操作,比如fetch_by_domain_domainroot方法。

浅谈DISCUZ X系列 数据库的操作