本節(jié)我們看下數(shù)據(jù)庫(kù)驅(qū)動(dòng)相關(guān)的源碼褪秀,本質(zhì)上來(lái)說(shuō)數(shù)據(jù)庫(kù)驅(qū)動(dòng)其實(shí)就是對(duì)適配器模式的應(yīng)用而已:在抽象層統(tǒng)一好暴露給外界的接口,在驅(qū)動(dòng)內(nèi)部封裝差異化的細(xì)節(jié)慕爬。
CI框架數(shù)據(jù)庫(kù)驅(qū)動(dòng)的架構(gòu)是由這幾部分構(gòu)成的:
- 數(shù)據(jù)庫(kù)連接台囱,源碼位于 DB.php 中氏淑。
- 緩存處理,源碼位于 DB_cache.php 中。
- 驅(qū)動(dòng)抽象層骇径,源碼位于 DB_driver.php 中躯肌。
- 用來(lái)建表的數(shù)據(jù)庫(kù)工廠,源碼位于 DB_forge.php 中破衔。
- 查詢(xún)構(gòu)造器清女,源碼位于 DB_query_builder.php 中。
- 查詢(xún)結(jié)果處理晰筛,源碼位于 DB_result.php 中嫡丙。
- 工具函數(shù),源碼位于 DB_utility.php 中读第。
- 以及一些驅(qū)動(dòng)器曙博,位于 systems/database/drivers 下。
由于這部分的源碼太多怜瞒,我們選擇性的將一些關(guān)鍵點(diǎn)的源碼分析一下父泳,只分析驅(qū)動(dòng) mysqli 的情況,主要圍繞如下幾點(diǎn):
- 驅(qū)動(dòng)是如何適配的吴汪?
- 數(shù)據(jù)庫(kù)是怎么連接上的惠窄?連接是怎么處理的?
- 原生的 sql 語(yǔ)句是怎么處理的浇坐?
- 為什么要有查詢(xún)構(gòu)造器睬捶?查詢(xún)構(gòu)造器是什么封裝的?
- 事務(wù)是怎么封裝的近刘?事務(wù)的自動(dòng)提交與回滾又是怎么做的擒贸?
- 數(shù)據(jù)庫(kù)緩存的是sql還是sql結(jié)果集?
驅(qū)動(dòng)加載
$this->load->database() 可以實(shí)現(xiàn)連接數(shù)據(jù)庫(kù)觉渴;位于加載器 Loader.php 中 介劫,該函數(shù)可以支持通過(guò)加載器自動(dòng)載入(也就是自動(dòng)連接數(shù)據(jù)庫(kù),之前在加載器源碼分析中看到過(guò))案淋,也可以支持手動(dòng)調(diào)用實(shí)現(xiàn)數(shù)據(jù)庫(kù)連接座韵。
/*
* $param 參數(shù)是可選的,它是數(shù)據(jù)庫(kù)配置踢京,如果沒(méi)傳誉碴,那么它就會(huì)讀取配置文件 database.php 中的配置。
* $return 決定是否返回?cái)?shù)據(jù)庫(kù)對(duì)象瓣距,該參數(shù)的意義在于當(dāng)你手動(dòng)連接多個(gè)數(shù)據(jù)庫(kù)時(shí)黔帕,你就不能再通過(guò)$this->db
* 獲取數(shù)據(jù)庫(kù)對(duì)象了,因?yàn)?this->db不知道你要引用哪個(gè)數(shù)據(jù)庫(kù),你只能在該函數(shù)中將數(shù)據(jù)庫(kù)返回蹈丸。
* $query_builder 決定是否啟用查詢(xún)構(gòu)造器成黄,因?yàn)樵谝恍╊?lèi)庫(kù)的函數(shù)中依賴(lài)查詢(xún)構(gòu)造器訪問(wèn)數(shù)據(jù)庫(kù)呐芥;該參數(shù)會(huì)覆蓋配置文件中的 query_builder。
* */
public function database($params = '', $return = FALSE, $query_builder = NULL)
{
// Grab the super object
$CI =& get_instance();
// 檢測(cè)數(shù)據(jù)庫(kù)是否已經(jīng)連接,如果已經(jīng)連接奋岁,直接返回
if ($return === FALSE && $query_builder === NULL && isset($CI->db) && is_object($CI->db) && ! empty($CI->db->conn_id))
{
return FALSE;
}
//真正的數(shù)據(jù)庫(kù)連接是在 DB.php 中
require_once(BASEPATH.'database/DB.php');
if ($return === TRUE)
{
return DB($params, $query_builder);
}
//數(shù)據(jù)庫(kù)連接成功后掛在全局對(duì)象$CI(也就是$this)上
$CI->db = '';
$CI->db =& DB($params, $query_builder);
return $this;
}
上面的代碼中我們看到思瘟,數(shù)據(jù)庫(kù)對(duì)象是從 DB.php 中生成的,該文件中我們將會(huì)看到是如何讀取數(shù)據(jù)庫(kù)配置以及適配驅(qū)動(dòng)的闻伶。
function &DB($params = '', $query_builder_override = NULL)
{
// 判斷是否通過(guò)參數(shù)傳進(jìn)了 dsn 配置滨攻,如果沒(méi)有就要讀配置文件
if (is_string($params) && strpos($params, '://') === FALSE)
{
//判斷配文文件是否存在
if ( ! file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/database.php')
&& ! file_exists($file_path = APPPATH.'config/database.php'))
{
show_error('The configuration file database.php does not exist.');
}
//加載配置文件
include($file_path);
//如果指定了要加載的目錄,還得去這些目錄下加載
if (class_exists('CI_Controller', FALSE))
{
foreach (get_instance()->load->get_package_paths() as $path)
{
if ($path !== APPPATH)
{
if (file_exists($file_path = $path.'config/'.ENVIRONMENT.'/database.php'))
{
include($file_path);
}
elseif (file_exists($file_path = $path.'config/database.php'))
{
include($file_path);
}
}
}
}
//檢車(chē)配置文件中的配置項(xiàng)是否存在
if ( ! isset($db) OR count($db) === 0)
{
show_error('No database connection settings were found in the database config file.');
}
//param參數(shù)有可能是空蓝翰,也有可能是某個(gè)配置組的key铡买,如果param不為空,說(shuō)明傳進(jìn)來(lái)的是某個(gè)配置組的key霎箍,
//后面就需要根據(jù)這個(gè)key選擇特定的配置組
if ($params !== '')
{
$active_group = $params;
}
//檢測(cè)$active_group 作為 key 的配置組是否存在
if ( ! isset($active_group))
{
show_error('You have not specified a database connection group via $active_group in your config/database.php file.');
}
elseif ( ! isset($db[$active_group]))
{
show_error('You have specified an invalid database connection group ('.$active_group.') in your config/database.php file.');
}
$params = $db[$active_group];
}
//通過(guò)參數(shù)傳進(jìn)了 dsn 格式的配置
elseif (is_string($params))
{
//解析 dsn奇钞,失敗的話就報(bào)錯(cuò),說(shuō)明是非法的連接字符串
if (($dsn = @parse_url($params)) === FALSE)
{
show_error('Invalid DB Connection String');
}
//這里為什么又要講 dsn 中的參數(shù)解析到 $param 中是因?yàn)槟J(rèn)讀取的配置文件中的配置以及通過(guò)手動(dòng)方式傳入的配置都是
//數(shù)組的格式漂坏,為了方便后續(xù)的處理景埃,將配置都同一成數(shù)組格式;這里還需注意:用 rawurldecode() 而非 urldecode()
//的原因是因?yàn)榍罢卟粫?huì)將加號(hào) + 解析成空格
$params = array(
'dbdriver' => $dsn['scheme'],
'hostname' => isset($dsn['host']) ? rawurldecode($dsn['host']) : '',
'port' => isset($dsn['port']) ? rawurldecode($dsn['port']) : '',
'username' => isset($dsn['user']) ? rawurldecode($dsn['user']) : '',
'password' => isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '',
'database' => isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : ''
);
// 解析 url 中的查詢(xún)字符串
if (isset($dsn['query']))
{
parse_str($dsn['query'], $extra);
foreach ($extra as $key => $val)
{
if (is_string($val) && in_array(strtoupper($val), array('TRUE', 'FALSE', 'NULL')))
{
$val = var_export($val, TRUE);
}
$params[$key] = $val;
}
}
}
// 如果既沒(méi)通過(guò)參數(shù)傳入配置顶别,也沒(méi)從配置文件讀到配置谷徙,那么就報(bào)錯(cuò)
if (empty($params['dbdriver']))
{
show_error('You have not selected a database type to connect to.');
}
// 是否啟用查詢(xún)構(gòu)造器
if ($query_builder_override !== NULL)
{
$query_builder = $query_builder_override;
}
// Backwards compatibility work-around for keeping the
// $active_record config variable working. Should be
// removed in v3.1
elseif ( ! isset($query_builder) && isset($active_record))
{
$query_builder = $active_record;
}
//載入驅(qū)動(dòng)抽象層,該類(lèi)中規(guī)范了所有驅(qū)動(dòng)對(duì)外的一致接口
require_once(BASEPATH.'database/DB_driver.php');
//判斷是否啟用查詢(xún)構(gòu)造器驯绎,然后將抽象驅(qū)動(dòng)層掛到 CI_DB 上完慧,后面的各種驅(qū)動(dòng)只需要繼承 CI_DB 統(tǒng)一對(duì)外行為就行了,
//注意: CI_DB_query_builder 繼承了 CI_DB_driver剩失,所以這里的 CI_DB 可看做是它兩的別名
if ( ! isset($query_builder) OR $query_builder === TRUE)
{
require_once(BASEPATH.'database/DB_query_builder.php');
if ( ! class_exists('CI_DB', FALSE))
{
/**
* CI_DB
*
* Acts as an alias for both CI_DB_driver and CI_DB_query_builder.
*
* @see CI_DB_query_builder
* @see CI_DB_driver
*/
class CI_DB extends CI_DB_query_builder { }
}
}
elseif ( ! class_exists('CI_DB', FALSE))
{
/**
* @ignore
*/
class CI_DB extends CI_DB_driver { }
}
//讀取配置中的驅(qū)動(dòng)屈尼,然后載入該驅(qū)動(dòng)
$driver_file = BASEPATH.'database/drivers/'.$params['dbdriver'].'/'.$params['dbdriver'].'_driver.php';
file_exists($driver_file) OR show_error('Invalid DB driver');
require_once($driver_file);
// 實(shí)例化該驅(qū)動(dòng)
$driver = 'CI_DB_'.$params['dbdriver'].'_driver';
$DB = new $driver($params);
//處理驅(qū)動(dòng)是 pdo 的情況,由于各個(gè)數(shù)據(jù)庫(kù)語(yǔ)法有不同的地方(主要是一些獲取元數(shù)據(jù)的語(yǔ)法)拴孤,所以將 pdo 又做了一層抽象脾歧,在它下面又劃分了一些子驅(qū)動(dòng)
if ( ! empty($DB->subdriver))
{
$driver_file = BASEPATH.'database/drivers/'.$DB->dbdriver.'/subdrivers/'.$DB->dbdriver.'_'.$DB->subdriver.'_driver.php';
if (file_exists($driver_file))
{
require_once($driver_file);
$driver = 'CI_DB_'.$DB->dbdriver.'_'.$DB->subdriver.'_driver';
$DB = new $driver($params);
}
}
//初始化,在該方法中進(jìn)行數(shù)據(jù)庫(kù)連接
$DB->initialize();
return $DB;
通過(guò)分析這段代碼我們看出驅(qū)動(dòng)的適配是通過(guò)讀取配置文件演熟,進(jìn)而加載相關(guān)的驅(qū)動(dòng)鞭执,驅(qū)動(dòng)實(shí)例化后通過(guò)調(diào)用了 $DB->initialize() 方法實(shí)現(xiàn)了數(shù)據(jù)連接 。
數(shù)據(jù)庫(kù)連接
我們知道數(shù)據(jù)庫(kù)連接是在 initialize()方法中實(shí)現(xiàn)的芒粹,接下來(lái)通過(guò)學(xué)習(xí)該方法的源碼你會(huì)看到如何處理數(shù)據(jù)庫(kù)連接兄纺,以及我們經(jīng)常看到的一個(gè)字眼 高可用化漆。
public function initialize()
{
//判斷是否已經(jīng)連接上了估脆,是的話,直接返回
if ($this->conn_id)
{
return TRUE;
}
// ----------------------------------------------------------------
/*
* 連接數(shù)據(jù)庫(kù)
*
* 注意:
* 接下來(lái)這行代碼體現(xiàn)出了面向?qū)ο蟮纳铄渲幓袢琲nitialize() 是在驅(qū)動(dòng)抽象層的旁蔼,
* 而當(dāng)驅(qū)動(dòng)繼承了驅(qū)動(dòng)抽象層,這里的 db_connect() 則是調(diào)用驅(qū)動(dòng)自己的 db_connect()疙教;
* 參數(shù)pconnect決定是否為持久連接棺聊,持久連接可以提升效率,但是持久連接不會(huì)在腳本
* 結(jié)束后釋放連接贞谓,如果連接數(shù)太多的時(shí)候會(huì)導(dǎo)致資源占用,影響別的客戶(hù)端限佩。
*
* */
$this->conn_id = $this->db_connect($this->pconnect);
// 連接的數(shù)據(jù)庫(kù),但是發(fā)現(xiàn)此連接失敗裸弦,那就檢測(cè)是否設(shè)置了故障轉(zhuǎn)移(高可用),
// 故障轉(zhuǎn)移是在配置文件的 failove 字段上設(shè)置的祟同,
// 具體的故障轉(zhuǎn)移配置見(jiàn):https://codeigniter.org.cn/user_guide/database/configuration.html
if ( ! $this->conn_id)
{
// Check if there is a failover set
if ( ! empty($this->failover) && is_array($this->failover))
{
// Go over all the failovers
foreach ($this->failover as $failover)
{
// Replace the current settings with those of the failover
foreach ($failover as $key => $val)
{
$this->$key = $val;
}
// Try to connect
$this->conn_id = $this->db_connect($this->pconnect);
// If a connection is made break the foreach loop
if ($this->conn_id)
{
break;
}
}
}
// 如果使用持久化連接以后依然失敗,那么報(bào)錯(cuò)
if ( ! $this->conn_id)
{
log_message('error', 'Unable to connect to the database');
if ($this->db_debug)
{
$this->display_error('db_unable_to_connect');
}
return FALSE;
}
}
// 最后設(shè)置字符編碼
return $this->db_set_charset($this->char_set);
}
這段代碼中我們看到了所謂的高可用方案理疙,通過(guò)在配置文件 database.php 設(shè)置 failover 實(shí)現(xiàn)的晕城,并且做了持久化連接的設(shè)置。
同時(shí)可看到真正的數(shù)據(jù)庫(kù)連接是下沉到各個(gè)驅(qū)動(dòng)中去的窖贤,因此我們才會(huì)看到了驅(qū)動(dòng)實(shí)現(xiàn)了各自的 db_connect() 方法砖顷。
驅(qū)動(dòng)中的數(shù)據(jù)庫(kù)連接就很簡(jiǎn)單了,解析連接參數(shù) 赃梧,然后調(diào)用相關(guān)的接口進(jìn)行數(shù)據(jù)庫(kù)連接罷了滤蝠;
public function db_connect($persistent = FALSE)
{
//判斷是否為socket連接
if ($this->hostname[0] === '/')
{
$hostname = NULL;
$port = NULL;
$socket = $this->hostname;
}
else
{
//非socket連接,根據(jù)參數(shù)配置選擇是否持久連接授嘀,注意:持久化連接僅在php5.3以上支持
$hostname = ($persistent === TRUE && is_php('5.3'))
? 'p:'.$this->hostname : $this->hostname;
$port = empty($this->port) ? NULL : $this->port;
$socket = NULL;
}
//接下來(lái)就是連接前的一些參數(shù)設(shè)置:超時(shí)時(shí)間物咳,客戶(hù)端壓縮之類(lèi)的
$client_flags = ($this->compress === TRUE) ? MYSQLI_CLIENT_COMPRESS : 0;
$mysqli = mysqli_init();
$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
if ($this->stricton)
{
$mysqli->options(MYSQLI_INIT_COMMAND, 'SET SESSION sql_mode="STRICT_ALL_TABLES"');
}
return $mysqli->real_connect($hostname, $this->username, $this->password, $this->database, $port, $socket, $client_flags)
? $mysqli : FALSE;
}
整個(gè)數(shù)據(jù)庫(kù)連接的過(guò)程就分析結(jié)束了, 數(shù)據(jù)庫(kù)連上后蹄皱,接下來(lái)就是和操作數(shù)據(jù)庫(kù)了览闰,先看下原生的 sql 語(yǔ)句是怎么被處理的。
原生 sql 語(yǔ)句處理
CI框架處理原生 sql 查詢(xún)的方法是 DB_driver.php 中的query方法巷折,在該方法中你會(huì)看到對(duì) sql 處理大致如下
首先判斷 sql 的行為焕济,原生的 sql 有兩種行為,要么是 read盔几,要么是 write晴弃,所以處理這兩種行為的返回結(jié)果是不一樣的,前者返回結(jié)果集逊拍,后者返回操作狀態(tài)上鞠!
對(duì)于 read 行為并且啟用了查詢(xún)緩存,則嘗試從緩存中讀取結(jié)果芯丧;如果沒(méi)有開(kāi)啟緩存芍阎,則通過(guò) simple_query() 獲取結(jié)果集對(duì)象并掛在 result_id 上,當(dāng)結(jié)果集對(duì)象獲取以后會(huì)扔給相應(yīng)驅(qū)動(dòng)的 DB_result 對(duì)象做一層數(shù)據(jù)訪問(wèn)的封裝缨恒,我們使用的 result_array()谴咸,row_array() 等獲取查詢(xún)結(jié)果的函數(shù)就是在 DB_result 中封裝的轮听!
最后將結(jié)果集對(duì)象(DB_result) 返回。
同時(shí)也會(huì)看到一些細(xì)節(jié)化的處理岭佳,比如
sql 執(zhí)行失敗事務(wù)該怎么辦血巍?
基準(zhǔn)測(cè)試的埋點(diǎn)是怎么埋的?
為什么 last_query() 能夠獲取到最近執(zhí)行的一條 sql?
那現(xiàn)在進(jìn)入 query 方法看下具體實(shí)現(xiàn)珊随。
public function query($sql, $binds = FALSE, $return_object = NULL)
{
//sql檢查述寡,判斷是否為空,并且獲取sql的操作類(lèi)型 read 還是 write
if ($sql === '')
{
log_message('error', 'Invalid query: '.$sql);
return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE;
}
elseif ( ! is_bool($return_object))
{
$return_object = ! $this->is_write_type($sql);
}
// Verify table prefix and replace if necessary
if ($this->dbprefix !== '' && $this->swap_pre !== '' && $this->dbprefix !== $this->swap_pre)
{
$sql = preg_replace('/(\W)'.$this->swap_pre.'(\S+?)/', '\\1'.$this->dbprefix.'\\2', $sql);
}
// 解析參數(shù)綁定
if ($binds !== FALSE)
{
$sql = $this->compile_binds($sql, $binds);
}
// 如果是讀類(lèi)型的sql叶洞,則先嘗試從緩存中讀取
if ($this->cache_on === TRUE && $return_object === TRUE && $this->_cache_init())
{
$this->load_rdriver();
if (FALSE !== ($cache = $this->CACHE->read($sql)))
{
return $cache;
}
}
// 記錄執(zhí)行過(guò)的sql鲫凶,last_query()就是從該數(shù)組中獲得最近一條sql的
if ($this->save_queries === TRUE)
{
$this->queries[] = $sql;
}
$time_start = microtime(TRUE);
//=================================================================================
//注意:這段邏輯有點(diǎn)復(fù)雜
/*
* 執(zhí)行sql,將結(jié)果集對(duì)象掛到result_id(result_id 會(huì)用在 DB_result 中封裝一層獲取數(shù)據(jù)的方法);
* 如果開(kāi)啟了事務(wù)但是執(zhí)行失敗衩辟,則回滾事務(wù)螟炫,由于事務(wù)存在嵌套的可能,_trans_depth 字段就是嵌套的層級(jí)艺晴,通過(guò) _trans_depth
* 一層一層的回滾事務(wù)不恭;最后打印出sql執(zhí)行出錯(cuò)的原因。
* */
if (FALSE === ($this->result_id = $this->simple_query($sql)))
{
if ($this->save_queries === TRUE)
{
$this->query_times[] = 0;
}
// This will trigger a rollback if transactions are being used
$this->_trans_status = FALSE;
// Grab the error now, as we might run some additional queries before displaying the error
$error = $this->error();
// Log errors
log_message('error', 'Query error: '.$error['message'].' - Invalid query: '.$sql);
if ($this->db_debug)
{
// We call this function in order to roll-back queries
// if transactions are enabled. If we don't call this here
// the error message will trigger an exit, causing the
// transactions to remain in limbo.
if ($this->_trans_depth !== 0)
{
do
{
$this->trans_complete();
}
while ($this->_trans_depth !== 0);
}
// Display errors
return $this->display_error(array('Error Number: '.$error['code'], $error['message'], $sql));
}
return FALSE;
}
//=============================================================================================
// Stop and aggregate the query time results
$time_end = microtime(TRUE);
$this->benchmark += $time_end - $time_start;
if ($this->save_queries === TRUE)
{
$this->query_times[] = $time_end - $time_start;
}
// Increment the query counter
$this->query_count++;
// 如果讀類(lèi)型的 sql 并不要求返回結(jié)果财饥,那么就要重置該 sql 對(duì)應(yīng)的緩存
if ($return_object !== TRUE)
{
if ($this->cache_on === TRUE && $this->cache_autodel === TRUE && $this->_cache_init())
{
$this->CACHE->delete();
}
return TRUE;
}
// 之前我們看到結(jié)果集對(duì)象是掛在 result_id 上换吧,通過(guò)load_rdriver加載相應(yīng)驅(qū)動(dòng) DB_result后,就可以將 $this 傳給它钥星,
//在其內(nèi)部獲取 result_id 后沾瓦,就可以封裝出一層結(jié)果訪問(wèn)函數(shù)了,如 row_array()谦炒,result_array() 等等
$driver = $this->load_rdriver();
$RES = new $driver($this);
//將結(jié)果集對(duì)象寫(xiě)入到緩存中贯莺,這里你可能會(huì)有疑問(wèn),$RES 已經(jīng)是結(jié)果集對(duì)象了宁改,為什么還要 new 一個(gè)結(jié)果集對(duì)象出來(lái)缕探?
//那是因?yàn)椋?dāng)腳本執(zhí)行完成后結(jié)果集要釋放了还蹲,但如果緩存是引用了某個(gè)結(jié)果集(也就是result_id)爹耗,勢(shì)必會(huì)導(dǎo)致
//該result_id無(wú)法釋放,進(jìn)而占用內(nèi)存資源
if ($this->cache_on === TRUE && $this->_cache_init())
{
$CR = new CI_DB_result($this);
$CR->result_object = $RES->result_object();
$CR->result_array = $RES->result_array();
$CR->num_rows = $RES->num_rows();
// Reset these since cached objects can not utilize resource IDs.
$CR->conn_id = NULL;
$CR->result_id = NULL;
$this->CACHE->write($sql, $CR);
}
//最后返回該結(jié)果集對(duì)象
return $RES;
}
load_rdriver()如下
public function load_rdriver()
{
$driver = 'CI_DB_'.$this->dbdriver.'_result';
//判斷相應(yīng)的 driver是否已經(jīng)被加載谜喊,如果沒(méi)有就需要加載進(jìn)來(lái)
if ( ! class_exists($driver, FALSE))
{
require_once(BASEPATH.'database/DB_result.php');
require_once(BASEPATH.'database/drivers/'.$this->dbdriver.'/'.$this->dbdriver.'_result.php');
}
return $driver;
}
我們說(shuō)了獲取結(jié)果集對(duì)象(result_id)后潭兽,會(huì)將該對(duì)象扔給 DB_result 對(duì)象封裝出一層數(shù)據(jù)訪問(wèn)的方法,例如 row_array()斗遏,result_array() 等等山卦,那么現(xiàn)在進(jìn)入 DB_result 中看看是不是這樣。
public function __construct(&$driver_object)
{
$this->conn_id = $driver_object->conn_id;
$this->result_id = $driver_object->result_id;
}
我們看到在構(gòu)造方法中真的獲取了結(jié)果集對(duì)象(result_id)诵次,看到 $driver_object 就是 query() 中 new driver($this) 時(shí)的數(shù)據(jù)庫(kù)對(duì)象账蓉,可以看出整個(gè)數(shù)據(jù)庫(kù)驅(qū)動(dòng)的架構(gòu)中責(zé)任劃分的還是很清楚的枚碗。
接著往下翻,我們看到了 result_object()
public function result_object()
{
//如果result_object還在內(nèi)存中铸本,就直接返回肮雨,意味著做了相同的查詢(xún)
if (count($this->result_object) > 0)
{
return $this->result_object;
}
if ( ! $this->result_id OR $this->num_rows === 0)
{
return array();
}
// 如果 result_array 已經(jīng)在內(nèi)存中了,由于數(shù)組和對(duì)象的區(qū)別不是很大归敬,直接通過(guò)array生成對(duì)象
if (($c = count($this->result_array)) > 0)
{
for ($i = 0; $i < $c; $i++)
{
$this->result_object[$i] = (object) $this->result_array[$i];
}
return $this->result_object;
}
//如果內(nèi)存中中沒(méi)有我們要獲取的數(shù)據(jù),那就調(diào)用_fetch_object()獲取結(jié)果鄙早,
//_fetch_object()是在各個(gè)驅(qū)動(dòng)器中實(shí)現(xiàn)的汪茧,因?yàn)槊總€(gè)驅(qū)動(dòng)器的結(jié)果集對(duì)象(result_id)不一樣
is_null($this->row_data) OR $this->data_seek(0);
while ($row = $this->_fetch_object())
{
$this->result_object[] = $row;
}
return $this->result_object;
}
在該方法內(nèi)部我們會(huì)看到其通過(guò) _fetch_object() 獲取了 sql 查詢(xún)結(jié)果,而這個(gè)結(jié)果恰恰是通過(guò)結(jié)果集對(duì)象(result_id)獲得的限番。
而 _fetch_object 其實(shí)就是對(duì)結(jié)果集對(duì)象(result_id)的包裝
protected function _fetch_object($class_name = 'stdClass')
{
return $this->result_id->fetch_object($class_name);
}
原生 sql 查詢(xún)的源碼分析就到此結(jié)束了舱污,同時(shí)我們?cè)诜治?query() 方法的過(guò)程中看到了緩存的處理,那么現(xiàn)在也就能解答一開(kāi)始我們關(guān)于緩存的疑問(wèn)了弥虐。
查詢(xún)緩存緩存的是sql還是結(jié)果集扩灯?
答:是結(jié)果集對(duì)象,結(jié)果集對(duì)象上持有了查詢(xún)結(jié)果霜瘪,而結(jié)果集對(duì)象被序列化后寫(xiě)入到以sql為文件名的文件中去的珠插!
我們?cè)?query() 看到結(jié)果集對(duì)象是通過(guò) DB_cache 對(duì)象的 write() 方法寫(xiě)入緩存中的,具體的處理過(guò)程如下:
public function write($sql, $object)
{
//可以看到緩存文件名是控制器颖对,方法捻撑,以及md5后的sql組成的
$segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1);
$segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2);
$dir_path = $this->db->cachedir.$segment_one.'+'.$segment_two.'/';
$filename = md5($sql);
if ( ! is_dir($dir_path) && ! @mkdir($dir_path, 0750))
{
return FALSE;
}
//看到?jīng)],結(jié)果集對(duì)象被序列化后寫(xiě)入到了緩存文件中去了
if (write_file($dir_path.$filename, serialize($object)) === FALSE)
{
return FALSE;
}
chmod($dir_path.$filename, 0640);
return TRUE;
}
我們知道 CI 框架還提供了一種讓我們操作數(shù)據(jù)庫(kù)的功能:查詢(xún)構(gòu)造器缤底,查詢(xún)構(gòu)造器對(duì)于不想寫(xiě)原生 sql 的人來(lái)說(shuō)是很方便的顾患,同時(shí)還能避免sql注入,但是高層的方便意味著底層的復(fù)雜个唧,下節(jié)我們看下查詢(xún)構(gòu)造器是怎么封裝的江解!