1. 程式人生 > >雲客Drupal8原始碼分析之資料庫系統及其使用

雲客Drupal8原始碼分析之資料庫系統及其使用

在開始本主題前請允許一點點題外話:

在我寫這個部落格的時候(2016年10月28日),《Begining Drupal 8》這本書已經翻譯完成並做成了PDF格式供給大家免費下載,這是一本引導新人學習drupal8的入門級教程,由drupal中文社群站http://drupalchina.cn/的站長龍馬組織翻譯,有20位奉獻者進行了大半年的工作得以完成,很榮幸我也是其中之一,用以進行這項工作的qq群號是:342823468,在這個群裡誕生了第一本drupal8中文教程,這件事真的很贊!群裡的20位翻譯者真的很贊!目前國內沒有一個由社群開發的php內容管理系統,而建立一個社群cms對大眾又是多麼有益,drupal在國際上如此流行,眾人聚焦精力對它精雕細琢造就了不錯的品質,延展使用範圍,快速迭代,以至於許多知名機構和公司用它做官網,而在國內儘管發展速度還不錯,但中文資料匱乏和缺乏系統整理嚴重影響了很多新人的步伐,這也是20位翻譯者無償勞動的意義所在,希望國內社群越來越大,這樣大家都有益處,一個人是創作不了LINUX那樣的偉業的,人多才能有生態,有生態才能反哺大家,這也是我寫雲客drupal8原始碼分析的一個願望,希望越來越多人加入這個社群。

好了,下面開始本篇的主題,主要講解drupal8資料庫系統的實現和使用方面的知識:

Symfony沒有資料庫元件,drupal8完全自己實現了一個基於php的pdo擴充套件的資料庫系統,它提供了一個數據庫抽象層,讓你可以使用統一的方式去操作資料庫,而不用管底層使用的是什麼資料庫,只需要使用好它提供的介面(物件方法或函式)就行,當需要更換另外型別的資料庫時,比如由MySQL換成Oracle或MS SQL Server,無需修改應用層程式碼。在釋出版中預設提供了mysql、pgsql、sqlite支援,
它也提供多臺資料庫伺服器支援,簡單的負載均衡很容易實現,在學習它之前建議你對以下內容有所瞭解:

1:php的PDO擴充套件,官方地址是:http://php.net/manual/zh/intro.pdo.php
這是php層面提供的資料庫抽象層,用以統一各類資料庫的操作,drupal8的資料庫系統是基於它實現的,瞭解它後在學習的過程中就可以清楚知道什麼事情是誰做的以及它們是怎麼配合的

2:SQL標準
SQL99也叫作SQL:1999、SQL3 、 SQL-99. 是sql語句的標準https://en.wikipedia.org/wiki/SQL:1999,還有ANSI SQL:2003,他們定義SQL語句的標準和資料庫應該具有的行為,瞭解這以後在看drupal8資料庫的sql語句構建時就不會迷惑了

3:軟體設計模式


在drupal8的設計中到處是模式,資料庫系統中裝飾者模式尤為突出,瞭解這後看程式碼會輕車熟路,會有很熟悉輕鬆的感覺,但不建議買大部頭的書看,網上很多帖子足以,幾個列子就能讓你明白,節省時間

下面先看一看資料庫連線資訊的定義:

定義位於站點配置檔案中/sites/default/settings.php,你可以看到類似下面的定義:

$databases['default']['default'] = array (
  'database' => 'drupal',
  'username' => 'yunke',
  'password' => 'yunke',
  'prefix' => 'yunke_',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

$databases['default']['default']的值是一個數組,這個陣列稱之為DSN(資料來源名稱Data Source Name),它代表一個數據庫,包含連線所需的所有資訊

為敘述簡單後面我們將這個陣列表示成$dsn

像上面的程式碼:$databases['default']['default']=$dsn;可能會讓你疑惑,為什麼是巢狀陣列?有什麼含義?

這就是drupal提供多資料庫支援的體現,以下我們用$databases[$key][$target]=$dsn;來表示

$key指一個數據庫,代表一個單獨系統,不同的$key通常是不同結構的資料庫,當和drupal以外的第三方系統協作時其他系統的資料庫就應該是不同的$key

預設的drupal只有一個$key,它被命名為default,它是drupal使用的資料庫

$target是什麼呢?它主要用於負載均衡,一個$key對應的資料庫,他們有相同的結構和資料,可以被分佈在多臺伺服器上面,以減輕壓力

那麼每一臺伺服器就對應一個$target,比如MySQL資料庫內建了一個主從備份的功能,在一個伺服器上面的MySQL例項可以快速同步到其他伺服器

那麼就可以有多個伺服器擁有相同的資料庫結構和資料,在非寫入查詢的時候能分攤訪問實現負載均衡,但寫入更新一類的查詢只能在主伺服器上面執行

(關於Mysql資料庫的主從複製推薦大家看《高效能MySQL》一書Baron Schwartz,Peter Zaitsev,Vadim Tkachenko 著;中文有售)

明白$databases['default']['default']的意思了吧,這也是大多數小型網站所需要的配置

表示有一個叫做default的資料庫,這個資料庫只有一個叫做default目標例項的伺服器在執行,所有讀寫操作都在這個伺服器上面

那麼怎麼配置多資料庫呢?可以這樣:

$databases['default']['default']=$dsn_1; //主伺服器
$databases['default']['replica']=$dsn_2; //用於只讀的從伺服器

上面實現了兩臺伺服器,那麼要實現多個從伺服器該怎麼操作?可以這樣:
$databases['default']['default']=$dsn_1; //主伺服器
$databases['default']['replica']=$dsn_2; //用於只讀的從伺服器
$databases['default']['yunke']=$dsn_3; //用於只讀的從伺服器

這樣雖然可以,但是每次獲得連結時都要指定$target很不方便,

況且同一個$key下面只有主伺服器可以寫,多個$target從伺服器是一樣的,且都是隻讀,所以基本使用下面的方式:

$databases['default']['default']=$dsn_1; //主伺服器
$databases['default']['replica'][]=$dsn_2; //用於只讀的從伺服器
$databases['default']['replica'][]=$dsn_3; //用於只讀的從伺服器
$databases['default']['replica'][]=$dsn_4; //用於只讀的從伺服器

drupal在一個$target下有多個$dsn時,它會隨機選擇一個,就實現了負載分攤,如無特別需要基本使用以上方式

注意:在一個$key下必須要有一個$target被命名為default,且它作為主伺服器,在程式內部當其他$target不可用時預設回退到default

關於寫查詢的負載均衡比較複雜,比如資料分片技術,需要應用層規劃,定義額外的鍵,請看上面推薦的書

明白了$key和$target後我們看一看$dsn這個陣列是怎麼定義的:

$dsn= array (
  'database' => 'drupal',
  'username' => 'yunke',
  'password' => 'yunke',
  'prefix' => 'yunke_',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

以上是最基本的資訊,大部分你應該能看懂,主要講以下內容:

表字首只有一個字串值的時候,表示所有資料表均使用該字首,不用請為空或者省略該陣列元素

很讚的是drupal支援為不同表運用不同字首,使用如下:

'prefix' => array("default"=>"預設字首","基本表名"=>"特定字首","基本表名"=>"特定字首"),

在程式內部就被轉化成這樣的格式,其中default必不可少,用於指定沒有特定字首的表預設使用的字首,不需要請='',

指定名字空間:'namespace'=> 'Drupal\\Core\\Database\\Driver\\mysql',

這個表示drupal資料庫抽象層使用特定資料庫驅動程式的名字空間,不加也可以,但最好加上,可提高程式速度,避免自動通過反射機制得到

在$dsn中還可以指定其他配置項,通常不同資料庫有不同配置,但也有一些通用配置,下面說說常用的MySQL可使用的配置項:

$dns['_dsn_utf8_fallback'] = TRUE
drupal預設使用utf8mb4資料庫字元編碼,它是utf8的超集,此指令表示回退到utf8編碼,不使用utf8mb4
預設使用utf8mb4,如果已經使用utf8mb4後不能回退到低版本字元編碼的資料庫,這樣會丟失超集部分的資料
詳見:http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
$dns['unix_socket']使用一個unix_socket
$dns['pdo']允許的pdo選項,用於控制pdo的行為
$dns['collation']執行設定mysql的字符集語句時:SET NAMES ' . $charset . ' COLLATE ' . $dns['collation'],沒有這個指令則設定:'SET NAMES ' . $charset
$dns['init_commands']執行mysql的初始化命令
$dns['transactions']表示是否支援事務,沒有設定則預設支援事務,除非明確設定為false以使強制不支援事務

以上就是關於如何定義資料庫連線選項的內容,下面說說drupal允許使用的資料庫命名規則:

drupal使用的資料庫名、表名、欄位名必須是字母、數字、下劃線和點號
判定規則是:preg_replace('/[^A-Za-z0-9_.]+/', '', $database);
他們的別名不能有點號(別名就是查詢語句as後面的名字),規則是preg_replace('/[^A-Za-z0-9_]+/', '', $field);

接下來看看怎麼進行資料庫查詢:

在整個drupal程式執行開始階段就把資料庫的配置資訊注入到了資料庫控制類中,由DrupalKernel完成呼叫
具體是在Drupal\Core\Site\Settings 的 initialize方法呼叫Database::setMultipleConnectionInfo($databases);

當需要查詢的時候首先需要獲得資料庫連線類,在模組中你可以這樣操作:

\Drupal::database(); //獲取配置中$databases['default']['default']表示的連結,這對於大多數只有一個數據庫的站點而言是最常用的,全域性獲取
\Drupal::service("database"); //完全等同於\Drupal::database();
$container()->get("database"); //效果同上,在容器物件可用時使用
\Drupal::service("database.replica"); //獲取配置中$databases['default']['replica']表示的備用資料庫連結,無設定將回退到主庫
$container()->get("database.replica"); //效果同上,在容器物件可用時使用
以上是常用的快捷方法,更加靈活的方法是:
\Drupal\Core\Database\Database::getConnection($target, $key); //這樣可以指定任意目標資料庫

在得到連結物件後就可以使用它做查詢了,在官方文件中查詢分為兩大類:靜態查詢、動態查詢

其實這個名字有些迷惑人,所謂靜態查詢就是直接寫SQL語句查詢,而動態查詢是呼叫系統提供的語句構建方法逐步構建SQL語句再查詢

先看一看所謂的靜態查詢,也就是直接寫SQL語句的查詢怎麼做:

$con=\Drupal::database();
$con->query("SELECT nid, title FROM {node} WHERE type = :type", array(':type' => 'page'));
$con->query("SELECT * FROM {node} WHERE nid IN (:nids[])", array(':nids[]' => array(13, 42, 144)));
在上面的SQL中表名均是被放在花括號裡面,是不帶字首的表名,系統依據這樣的語法自動新增表字首,無{}不會新增字首
花括號裡面不能有空格,兩側需要留至少一個空格,在SQL語句中其他部分不能使用{},僅僅被表名使用
也看到可以使用引數傳遞,引數佔位符名以冒號開始不加引號,在關聯陣列中元素順序沒有關係,這樣的設計系統自動防止注入攻擊,無需轉義引數值
drupal8可以讓我們使用陣列方式的佔位符,看上面最後一條語句,這在PDO中是不允許的,drupal會為我們自動展開陣列

靜態查詢雖然直截了當,但有兩個缺點:

第一:它無法讓模組查詢鉤子對SQL語句進行修改

第二:直接寫的SQL語句可能不會相容所有的資料庫,那麼在更換資料庫型別的時候帶來麻煩

所以基於上面的原因,推薦使用動態查詢,drupal為每一種型別的查詢都準備了一個類,它有許多方法來幫助構建查詢語句,比如:

$con=\Drupal::database();
$query = $con->select('users', 'u')
  ->condition('u.uid', 0, '<>')
  ->fields('u', array('uid', 'name', 'status', 'created', 'access'))
  ->range(0, 50);
$result = $query->execute();
foreach ($result as $record) {
  // Do something with each $record
}

動態查詢類似於CI框架的活動記錄類,它不需要懂得SQL怎麼寫,根據提供的方法構建即可

在這個例子中$con->select()方法返回一個select查詢構建物件,這個物件採用鏈式呼叫自己的方法幫助最終產生一個查詢sql

如果方法返回的是物件本身則可使用鏈式呼叫,資料庫連線物件可以返回多種查詢構建物件,他們有不同的幫助方法,如下:

$con->select($table, $alias = NULL, array $options = array()); //構建查詢物件
$con->insert($table, array $options = array()); //構建插入SQL語句物件
$con->merge($table, array $options = array()); //構建合併查詢
$con->upsert($table, array $options = array()); //構建upset查詢物件,資料庫不支援則模擬
$con->update($table, array $options = array()); //構建更新查詢物件
$con->delete($table, array $options = array());  //構建刪除查詢物件
$con->truncate($table, array $options = array());  //構建清空資料表查詢物件

這些查詢構建物件的定義在這裡:\core\lib\Drupal\Core\Database\Driver\,
 它們繼承自母類\core\lib\Drupal\Core\Database\Query,母類提供各資料庫相同功能,子類解決不同資料庫的特殊性
這些查詢構建類各自定義了很多方法去構建自己型別的sql查詢,這些sql語句是滿足資料庫型別的,
上面的這些查詢構建物件都可以通過強制型別轉換(string)$var來得到構建的SQL語句,在他們內部用php魔術方法__toString()來實現此目的,實際上最終就是使用該方法得到SQL語句並傳遞給Connection連線物件的中心查詢方法去執行。

要把每個構建物件及他們的構建方法介紹完,需要很大篇幅,這裡不做介紹,你可以到官網文件檢視,如需深入理解直接看類定義程式碼吧,它有詳細的註釋,官網的API文件就提取自這些註釋。

動態查詢除了幫助構建相容的SQL外還可以新增查詢標籤,這個功能可以讓drupal模組據此修改相應的查詢SQL

建立資料庫及表結構:

要建立一個數據庫,以及定義裡面的表結構,在drupal中往往不是通過靜態查詢功能實現的,drupal資料庫連結物件提供了一個方法來返回schema物件:

schema物件操作資料庫結構定義,這是一塊很重要的內容,由於篇幅有限,將在下一篇原始碼分析中專門介紹,基本使用如下:

$con=\Drupal::database();
$con->schema();
開啟資料庫事務:

資料庫事務讓查詢具備原子性、一致性,在drupal中預設是支援事務的,除非在連結選項中明確禁止事務,mysql預設使用支援事務處理的InnoDB儲存引擎

$con=\Drupal::database();
$yunke=$con->startTransaction($name = ''); //開始事務,引數指回滾點,只要變數$yunke不被銷燬那麼事務持續開啟,一旦銷燬即被提交,原理是事務物件失去變數引用時,被php銷燬,執行了解構函式。
$con->rollback($savepoint_name = 'drupal_transaction')  //回滾事務到某個回滾點
$yunke=NULL;//變數被銷燬,事務被提交
不要使用$con->commit();去提交一個事務,這會丟擲異常,系統會隱式自動提交,
需要注意的是DDL語句(資料庫定義語句)在大多數資料庫中是不支援事務的,包括MYSQL

使用結果集:

SELECT查詢返回一個或多個數據,這些結果集被包裝在Drupal\Core\Database\Statement中,它繼承自PDO的PDOStatement類,可以使用它的全部方法,行為受到查詢選項的控制,通常使用foreach迴圈去獲取結果,如下:

$con=\Drupal::database();
$result = $con->query("SELECT nid, title FROM {node}");
foreach ($result as $record) {
  // Do something with each $record
  $node = node_load($record->nid);
}
$record = $result->fetch();            // 使用預設fetch模式獲取下一行資料.
$record = $result->fetchObject();  // 以stdClass物件形式取回
$record = $result->fetchAssoc(); //以陣列方式取回
$record = $result->fetchField($column_index); //獲取一行中的一個欄位,$column_index是列索引值,以0開始
$number_of_rows = $result->rowCount(); //計算 DELETE、INSERT 、UPDATE 影響的行數,SELECT不應該使用這個方法
$con->select('users')->countQuery()->execute()->fetchField(); //SELECT應該使用這個方法,在countQuery()呼叫前需要構建好查詢

資料庫操作的過程式包裝:

php的函式是全域性可用的,為了方便操作,drupal把許多資料庫操作功能封裝到函式中,這樣就可以在任何地方呼叫了

這些函式定義在/core/includes/database.inc和/core/includes/schema.inc中,它們在HTTP核心堆疊的預處理層被載入

你可以在模組中直接使用,詳情見這兩個檔案

上面基本講到使用層面的內容,下面我們看看原始碼佈局:

在drupal中資料庫原始碼位於:\core\lib\Drupal\Core\Database,
其中Driver子目錄存放不同資料庫的驅動,解決差異問題(資料庫方言),Query子目錄存放SQL查詢語句構建類,專門構建SQL語句,所有的執行彙總到連結類Connection的query方法上,而此方法還不是真正執行查詢的方法,檢視原始碼:

$stmt = $this->prepareQuery($query);
$stmt->execute($args, $options);
這個$stmt其實是Drupal\Core\Database\Statement物件,為什麼是這個?在連結物件建構函式中有:
$connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
這個$this->statementClass就指定了Drupal\Core\Database\Statement,這個設定就是讓pdo返回這個物件
如果還不明白,請看:http://php.net/manual/zh/pdo.setattribute.php

所以最終執行drupal所有查詢的是Drupal\Core\Database\Statement的execute方法

查詢日誌記錄就設定在這個方法裡面,如果需要開啟查詢日誌記錄可以這樣:

\Drupal\Core\Database\Database::startLog($logging_key, $key = 'default'); 
//開始日誌記錄 配置中的一個$key對應一個日誌記錄器,$logging_key可以是$target也可以自己隨意指定
\Drupal\Core\Database\Database::getLog($logging_key, $key = 'default');  //得到查詢日誌
查詢日誌是一個數組,內容如下:
array(
        'query' => "查詢語句",
        'args' => "引數",
        'target' => $target,
        'caller' => "呼叫者",
        'time' => "查詢這條語句執行的時間",
      );
在除錯的時候使用它非常方便,可以用drupal的日誌系統去儲存這個日誌

如何將drupal的資料庫系統提取出來?

如果你覺得drupal的資料庫系統很好,想提取出來用在自己其他的專案上面,你將需要處理下面的工作:

1:drupal資料庫程式碼在資料庫中建立了一個sequences表,用於滿足nextId()的功能,提供一個比之前返回的數大的唯一整數
2:在durpal中模組可以修改查詢,資料庫程式碼對查詢提供標籤管理並觸發模組修改,在preExecute方法中呼叫
\Drupal::moduleHandler()->alter($hooks, $query);修改查詢語句,模組通過這些標籤修改查詢,

處理好這些和drupal系統緊密耦合的地方就可以提取出來單獨使用了

補充知識點:

1:可以使用db_ignore_replica()函式禁用從庫,此函式位於core\includes\database.inc,在配置檔案中可以設定maximum_replication_lag來指定從庫的延遲時間,不設定預設為300秒,這個函式會建立$_SESSION['ignore_replica_server'],在後續頁面中根據此判斷是否禁用從庫,它的值是設定時的請求時間加延遲時間,在這個時間內系統不會使用從庫,此判斷是在核心派發kernel.request事件時進行
2:現在已經不再使用術語"master/slave"而使用"primary/replica",由於文化原因,請看:https://www.drupal.org/node/2275877

以上就是drupal8資料庫程式碼的所有知識,不盡之處相信已經可以輕鬆通過原始碼或官方文件查清楚了
下一篇將介紹資料庫的Schema,它是一個API,介紹如何在drupal的應用層定義資料庫,如何建庫建表

我是雲客,【雲遊天下,做客四方】,微信號:php-world,歡迎轉載,但須註明出處,討論請加qq群203286137