# 第一章:數(shù)據(jù)庫架構基礎
本章我們首先從`ThinkPHP5.0`的數(shù)據(jù)庫訪問層架構設計原理開始,然后熟悉下數(shù)據(jù)庫的配置,并掌握如何進行基礎的查詢操作,并簡單介紹了分布式、存儲過程及事務,學習內(nèi)容主要包括:
[TOC=2,2]
## 數(shù)據(jù)庫架構設計
使用框架開發(fā)應用,一般不需要直接操作數(shù)據(jù)庫,而是通過框架封裝好的數(shù)據(jù)庫中間層對數(shù)據(jù)庫進行操作。這樣的好處主要有兩個:一是簡化數(shù)據(jù)庫操作,二是做到跨數(shù)據(jù)庫的一致性。這種設計的中間層通常稱之為數(shù)據(jù)庫訪問抽象層,簡稱數(shù)據(jù)訪問層(`DAL`),ThinkPHP5的數(shù)據(jù)訪問層是基于PHP內(nèi)置的`PDO`對象實現(xiàn)。一般抽象層本身并不直接操作數(shù)據(jù)庫,而是通過驅動來實現(xiàn)具體的數(shù)據(jù)庫操作。
`ThinkPHP5.0`的數(shù)據(jù)庫設計相比之前版本更加合理,數(shù)據(jù)訪問層劃分的更細化,把數(shù)據(jù)訪問對象分成了連接器、查詢器、生成器等多個對象,并通過數(shù)據(jù)庫訪問入口類統(tǒng)一調(diào)用,分工更明確,各司其職,欲知詳情且聽我慢慢道來。
ThinkPHP數(shù)據(jù)訪問層設計示意圖:

> 5.1版本的架構略微進行了一些調(diào)整,變成:

### 數(shù)據(jù)庫入口類`Db`
平常我們的數(shù)據(jù)庫操作使用的類庫一般都是數(shù)據(jù)庫的入口類`think\Db`。這個類非常的簡單,主要就是一個`connect`方法,根據(jù)數(shù)據(jù)庫配置參數(shù)連接數(shù)據(jù)庫(注意這里的連接并非真正的連接數(shù)據(jù)庫,只是做好了隨時連接的準備工作,只有在實際查詢的時候才會真正去連接數(shù)據(jù)庫,是一種惰性連接)并獲取到數(shù)據(jù)庫連接對象的實例。
`Db`類都是靜態(tài)方法調(diào)用,但看起來這個類啥都沒實現(xiàn),那是怎么操作數(shù)據(jù)庫的呢,其實就是封裝了數(shù)據(jù)庫操作方法的**靜態(tài)調(diào)用**(利用`__callStatic`方法),下面是代碼實現(xiàn):
~~~
// 調(diào)用驅動類的方法
public static function __callStatic($method, $params)
{
// 自動初始化數(shù)據(jù)庫
return call_user_func_array([self::connect(), $method], $params);
}
~~~
理論上來說,框架并不依賴`Db`類,該類的存在只是為了簡化數(shù)據(jù)庫抽象層的操作而提供的一個工廠類,否則你就需要單獨實例化不同的數(shù)據(jù)庫連接類。因此,看似可有可無的`Db`類就成了數(shù)據(jù)訪問層實現(xiàn)的點睛之筆了^_^
>[danger] 所有的數(shù)據(jù)庫操作都是經(jīng)過`Db`類調(diào)用,并且`Db`類是一個靜態(tài)類,但`Db`類自身只有一個公共方法`connect`。
### 連接器類`Connection`
顧名思義,連接類的作用就是連接數(shù)據(jù)庫,也稱為連接器。我們知道,不同的數(shù)據(jù)庫的連接方式和參數(shù)都是不同的,連接類就是要解決這個差異問題。
數(shù)據(jù)庫入口類里面實例化的類其實就是對應數(shù)據(jù)庫的連接類,連接類的基類是`think\db\Connection`。例如,需要連接`Mysql`數(shù)據(jù)庫的話,就必須定義一個`Mysql`連接類(內(nèi)置由`think\db\connector\Mysql`類實現(xiàn),繼承了`think\db\Connection`類),當然具體的連接類名沒有固定的規(guī)范(例如,`MongoDb`的連接類就是`think\mongo\Connection`)。如果某個數(shù)據(jù)庫的連接擴展類沒有繼承`think\db\Connection`,那就意味著所有的數(shù)據(jù)庫底層操作有可能被接管,在個別特殊的數(shù)據(jù)庫的擴展中就有類似的實現(xiàn),例如`MongoDb`數(shù)據(jù)庫擴展。
>[danger] 數(shù)據(jù)庫連接都是惰性的,只有最終執(zhí)行SQL的時候才會進行連接。
連接器是數(shù)據(jù)訪問層的基礎,基于PHP本身的`PDO`實現(xiàn)(如果你還不了解`PDO`,請參考PHP官方手冊中[PDO](http://php.net/manual/zh/book.pdo.php)部分,不在本書的討論范疇),連接類的主要作用就是連接具體的數(shù)據(jù)庫,以及完成基本的數(shù)據(jù)庫底層操作,包括對分布式、存儲過程和事務的完善處理。而更多的數(shù)據(jù)操作則交由查詢類完成。
框架內(nèi)置的連接類包括:
|數(shù)據(jù)庫|連接類|
|---|---|
|Mysql|think\db\connector\Mysql|
|Pgsql|think\db\connector\Pgsql|
|Sqlite|think\db\connector\Sqlite|
|Sqlsrv|think\db\connector\Sqlsrv|
>如果是僅僅使用原生SQL查詢的話,只需要使用連接類就可以了(通過調(diào)用Db類完成)
連接器類的作用小結:
* 連接數(shù)據(jù)庫;
* 獲取數(shù)據(jù)表和字段信息;
* 基礎查詢(原生查詢);
* 事務支持;
* 分布式支持;
### 查詢器類`Query`
除了基礎的原生查詢可以在連接類完成之外,其它的查詢操作都是調(diào)用查詢類的方法,查詢類內(nèi)完成了數(shù)據(jù)訪問層最重要的工作,銜接了連接類和生成類,統(tǒng)一了數(shù)據(jù)庫的查詢用法,所以查詢類是不需要單獨驅動配合的,我們也稱之為查詢器。無論采用什么數(shù)據(jù)庫,我們的查詢方式是統(tǒng)一的,因為數(shù)據(jù)訪問層核心只有一個唯一的查詢類:`think\db\Query`。
`Query`類封裝了所有的數(shù)據(jù)庫`CURD`方法的優(yōu)雅實現(xiàn),包括鏈式方法及各種查詢,并自動使用了`PDO`參數(shù)綁定(參數(shù)自動綁定是在生成器類解析生成SQL時完成),最大程度地保護你的程序避免受數(shù)據(jù)庫注入攻擊,查詢操作會調(diào)用生成類生成對應數(shù)據(jù)庫的SQL語句,然后再調(diào)用連接類提供的底層原生查詢方法執(zhí)行最終的數(shù)據(jù)庫查詢操作。
>[danger] 所有的數(shù)據(jù)庫查詢都使用了`PDO`的預處理和參數(shù)綁定機制。你所看到的大部分數(shù)據(jù)庫方法都來自于查詢類而并非`Db`類,這一點很關鍵,也就是說雖然我們始終使用`Db`類操作數(shù)據(jù)庫,而實際上大部分方法都是由查詢器類提供的方法。
### 生成器類`Builder`
生成類的作用是接收`Query`類的所有查詢參數(shù),并負責解析生成對應數(shù)據(jù)庫的原生`SQL`語法,然后返回給`Query`類進行后續(xù)的處理(包括交給連接類進行`SQL`執(zhí)行和返回結果處理),也稱為(語法)生成器。生成類的作用其實就是解決不同的數(shù)據(jù)庫查詢語法之間的差異。查詢類實現(xiàn)了統(tǒng)一的查詢接口,而生成類負責數(shù)據(jù)庫底層的查詢對接。
>[danger] 生成類一般不需要自己調(diào)用,而是由查詢類自動調(diào)用的。也可以這么理解,生成類和查詢類是一體的,事實上它們合起來就是通常我們所說的查詢構造器(因為實際的查詢操作還是在連接器中執(zhí)行的)。
通常每一個數(shù)據(jù)庫連接類都會對應一個生成類,框架內(nèi)置的生成類包括:
|數(shù)據(jù)庫|生成類|
|---|---|
|Mysql|think\db\builder\Mysql|
|Pgsql|think\db\builder\Pgsql|
|Sqlite|think\db\builder\Sqlite|
|Sqlsrv|think\db\builder\Sqlsrv|
這些生成類都繼承了核心提供的生成器基類`think\db\Builder`,每個生成器類只需要提供差異部分的實現(xiàn)。
## 數(shù)據(jù)庫配置
數(shù)據(jù)庫的配置參數(shù)有很大的學問,也是你掌握數(shù)據(jù)庫操作的基礎,主要用于數(shù)據(jù)庫的連接以及查詢的相關設置。
數(shù)據(jù)庫的配置參數(shù)用于連接類的架構方法,而由于我們并不直接操作連接類,所以,配置參數(shù)主要通過`Db`類傳入并設置到當前的數(shù)據(jù)庫連接類。
數(shù)據(jù)庫配置分為**靜態(tài)配置**和**動態(tài)配置**兩種方式,靜態(tài)配置是指在數(shù)據(jù)庫配置文件中進行配置,動態(tài)配置是指在Db類或者`Query`類的`connect`方法中傳入動態(tài)的配置參數(shù)。
安裝好ThinkPHP5之后,默認在`application`目錄下面會有一個`database.php`文件,這就是應用的數(shù)據(jù)庫配置文件,如果你的模塊需要單獨的數(shù)據(jù)庫配置文件,那么只需要在模塊目錄下面創(chuàng)建一個`database.php`文件即可,并且只需要定義和應用數(shù)據(jù)庫配置文件有差異的部分。
>[danger] 數(shù)據(jù)庫配置文件中配置的是默認的數(shù)據(jù)庫連接配置,如果你有多個數(shù)據(jù)庫連接,額外的數(shù)據(jù)庫連接是在應用配置文件中完成的(參考后面的動態(tài)數(shù)據(jù)庫連接)。
~~~
├─application
│ ├─index
│ │ ├─database.php (模塊)數(shù)據(jù)庫配置文件
│ │ └─ ...
│ ├─database.php (應用)數(shù)據(jù)庫配置文件
│ └─ ...
~~~
> 我們下面的數(shù)據(jù)庫配置文件都以應用數(shù)據(jù)庫配置文件為例說明。
默認的應用數(shù)據(jù)庫配置文件如下:
~~~
return [
// 數(shù)據(jù)庫類型
'type' => 'mysql',
// 服務器地址
'hostname' => '127.0.0.1',
// 數(shù)據(jù)庫名
'database' => '',
// 用戶名
'username' => 'root',
// 密碼
'password' => '',
// 端口
'hostport' => '',
// 連接dsn
'dsn' => '',
// 數(shù)據(jù)庫連接參數(shù)
'params' => [],
// 數(shù)據(jù)庫編碼默認采用utf8
'charset' => 'utf8',
// 數(shù)據(jù)庫表前綴
'prefix' => '',
// 數(shù)據(jù)庫調(diào)試模式
'debug' => true,
// 數(shù)據(jù)庫部署方式:0 集中式(單一服務器),1 分布式(主從服務器)
'deploy' => 0,
// 數(shù)據(jù)庫讀寫是否分離 主從式有效
'rw_separate' => false,
// 讀寫分離后 主服務器數(shù)量
'master_num' => 1,
// 指定從服務器序號
'slave_no' => '',
// 是否嚴格檢查字段是否存在
'fields_strict' => true,
// 數(shù)據(jù)集返回類型
'resultset_type' => 'array',
// 自動寫入時間戳字段
'auto_timestamp' => false,
// 時間字段取出后的默認時間格式
'datetime_format' => 'Y-m-d H:i:s',
// 是否需要進行SQL性能分析
'sql_explain' => false,
// Builder類
'builder' => '',
// Query類
'query' => '\\think\\db\\Query',
];
~~~
最關鍵的參數(shù)就是下面幾個(其它參數(shù)后面會陸續(xù)涉及):
|參數(shù)名|作用|
|---|---|
|type|數(shù)據(jù)庫類型或者連接類名|
|hostname|數(shù)據(jù)庫服務器地址(一般是IP地址,默認為`127.0.0.1`)|
|username|數(shù)據(jù)庫用戶名(默認為`root`)|
|password|數(shù)據(jù)庫用戶密碼(默認為空)|
|database|使用的數(shù)據(jù)庫名稱|
|charset|數(shù)據(jù)庫編碼(默認為`utf8`)|
`type`參數(shù)嚴格來說其實配置的是連接類名(而不是數(shù)據(jù)庫類型),支持命名空間完整定義,不帶命名空間定義的話,默認采用`\think\db\connector`作為命名空間(內(nèi)置連接類的命名空間)。你完全可以在應用中擴展自己的數(shù)據(jù)庫連接類,例如配置為:
~~~
// 配置數(shù)據(jù)庫類型(連接類)為自定義
'type' => '\app\db\Mysql',
~~~
這樣就可以自己替換或者擴展一些額外的數(shù)據(jù)庫操作方法。
>[danger] 自定義連接類的時候,請注意設置數(shù)據(jù)庫配置中的`builder`參數(shù)避免找不到對應生成器類。
`ThinkPHP5.0`采用PDO來統(tǒng)一操作數(shù)據(jù)庫,而連接類的最關鍵的作用就是通過配置連接到數(shù)據(jù)庫,PDO的連接方法參數(shù)如下:
>[info] #### PDO::__construct ( 'DSN' ,'用戶名','密碼','連接參數(shù)(數(shù)組)' )
數(shù)據(jù)庫的數(shù)據(jù)源名稱(`DSN`)是最關鍵的一個參數(shù),連接類負責把數(shù)據(jù)庫配置參數(shù)自動轉換為一個有效的`DSN`數(shù)據(jù)源名稱。如果你有特殊的連接語法需求,則可以通過配置數(shù)據(jù)庫配置文件中的`dsn`參數(shù)來解決,該配置參數(shù)的值會直接用于PDO連接,例如:
~~~
// 連接dsn
'dsn' => 'mysql:unix_socket=/tmp/mysql.sock;dbname=demo',
~~~
數(shù)據(jù)庫支持斷線重連機制(默認關閉),可以設置(`V5.0.6+`版本僅支持Mysql數(shù)據(jù)庫,`V5.0.9+`版本開始支持內(nèi)置所有數(shù)據(jù)庫):
~~~
// 開啟斷線重連
'break_reconnect' => true,
~~~
除了`DSN`數(shù)據(jù)源名稱,`PDO`的連接參數(shù)也可以單獨設置,每個連接驅動都有自己的連接參數(shù)設置,`Mysql`連接器內(nèi)置采用的參數(shù)包括如下:
~~~
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
~~~
可以在數(shù)據(jù)庫配置文件中設置`params`參數(shù),會和內(nèi)置的連接參數(shù)合并,例如:
~~~
// 數(shù)據(jù)庫連接參數(shù)
'params' => [
// 使用長連接
\PDO::ATTR_PERSISTENT => true,
// 數(shù)據(jù)表字段統(tǒng)一轉換為小寫
\PDO::ATTR_CASE => \PDO::CASE_LOWER,
],
~~~
常用數(shù)據(jù)庫連接參數(shù)(params)可以參考[PHP在線手冊](http://php.net/manual/zh/pdo.constants.php)中的以`PDO::ATTR_`開頭的常量。
## 如何開始查詢
在開始學習查詢之前,我們首先在`demo`數(shù)據(jù)庫中創(chuàng)建一個`data`測試表。
~~~
CREATE TABLE IF NOT EXISTS `data`(
`id` int(8) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '名稱',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
~~~
然后設置數(shù)據(jù)庫配置文件內(nèi)容為(如果有密碼請自行修改):
~~~
return [
// 數(shù)據(jù)庫類型
'type' => 'mysql',
// 服務器地址
'hostname' => '127.0.0.1',
// 數(shù)據(jù)庫名
'database' => 'demo',
// 用戶名
'username' => 'root',
// 密碼
'password' => '',
// 開啟數(shù)據(jù)庫調(diào)試
'debug' => true,
];
~~~
> 特別注意我們在配置中開啟了`debug`參數(shù),表示開啟數(shù)據(jù)庫的調(diào)試模型,開啟后會記錄數(shù)據(jù)庫的連接信息和SQL日志,數(shù)據(jù)庫的調(diào)試模式和應用的調(diào)試模式是兩個不同的概念。
配置完數(shù)據(jù)庫連接信息后,我們就可以直接使用`Db`類進行數(shù)據(jù)庫運行原生SQL操作了,你無需關心數(shù)據(jù)庫的連接操作,系統(tǒng)會自動使用數(shù)據(jù)庫配置參數(shù)進行數(shù)據(jù)庫的連接操作。
`Db`類的方法都是靜態(tài)調(diào)用(不需要去實例化`think\Db`類),`Db`類的查詢方法有很多(大部分查詢都是使用的查詢構造器),本章內(nèi)容暫時只講兩個用于原生查詢的方法,包括`query`(查詢操作)和`execute`(寫入操作),更多的查詢方法會在查詢構造器章節(jié)作出詳細講解。
數(shù)據(jù)庫查詢的所有示例都需要寫到一個控制器的方法里面,我們現(xiàn)在假設你已經(jīng)定義了一個下面的控制器操作方法:
~~~
<?php
namespace app\index\controller;
use think\Db;
class Index
{
public function index()
{
// 這里是數(shù)據(jù)庫操作的測試代碼
// ...
return;
}
}
~~~
> 一般來說并不建議在控制器的操作方法中直接操作數(shù)據(jù)庫Db類,但由于我們還沒涉及到模型章節(jié)的內(nèi)容,因此,目前的寫法僅為了演示數(shù)據(jù)庫的示例代碼。
并且在應用配置文件中開啟頁面Trace顯示:
~~~
// 應用Trace
'app_trace' => true,
~~~
> 開啟頁面Trace的作用是為了方便我們查看當前請求的SQL語句信息以及執(zhí)行時間(需要開啟數(shù)據(jù)庫調(diào)試模式后有效)。
然后在`index`操作方法中添加下面測試代碼:
~~~
Db::execute('insert into data (id, name) values (1, "hinkphp")');
Db::query('select * from data where id=1');
~~~
> 對數(shù)據(jù)表的CURD操作,除了`select`和存儲過程調(diào)用使用`query`方法之外,其它的操作都使用`execute`方法,這里就不再一一演示了。
訪問頁面后,顯示空白,點擊右下角的 
就可以打開頁面Trace信息,切換到SQL一欄,可以看到下面的類似信息

第一條表示數(shù)據(jù)庫的連接信息(連接消耗時間以及連接的DSN),后面的兩條就表示當前操作執(zhí)行的SQL語句,由于我們使用的是原生查詢,所以SQL語句和你的代碼里面的SQL語句是一致的,每條SQL語句最后會顯示該SQL語句的執(zhí)行消耗時間。
細心的朋友會發(fā)現(xiàn)`Db`類里面并沒有`query`和`execute`方法,其實在調(diào)用`Db`類的方法(`connect`方法除外)之前,都會先調(diào)用`connect`方法進行數(shù)據(jù)庫的初始化(前面提過的`__callStatic`方法),由于`connect`方法會返回一個數(shù)據(jù)庫連接類的對象實例(根據(jù)配置參數(shù)實現(xiàn)了單例),所以`Db`類調(diào)用的`query`和`execute`方法其實就是連接器類(`Connection`)的方法,這一點必須理解,否則你很難理解數(shù)據(jù)庫的查詢操作。
## 使用參數(shù)綁定
上面的例子是實際開發(fā)中其實并不建議,原則上我們在使用原生查詢的時候最好使用參數(shù)綁定避免SQL注入,例如:
~~~
Db::execute('insert into data (id, name) values (?, ?)',[2,'kancloud']);
Db::query('select * from data where id=?',[2]);
~~~
頁面Trace信息中會顯示實際運行的SQL語句

也支持命名占位符綁定,例如:
~~~
Db::execute('insert into data (id, name) values (:id, :name)',['id'=>3,'name'=>'topthink']);
Db::query('select * from data where id=:id',['id'=>3]);
~~~
>[danger] 參數(shù)綁定的變量不需要使用引號
同樣顯示的實際執(zhí)行SQL如下:

我們看到查詢語句中的id的值是字符串的,由于參數(shù)綁定默認都是使用的字符串,如果需要指定為數(shù)字類型,可以使用下面的方式:
~~~
Db::execute('insert into data (id, name) values (:id, :name)',['id'=>[4,\PDO::PARAM_INT],'name'=>'onethink']);
Db::query('select * from data where id=:id',['id'=>[4,\PDO::PARAM_INT]]);
~~~
這次查看實際的執(zhí)行SQL會有細微的變化

PDO命名占位綁定不支持一個參數(shù)多處綁定,下面的用法會報錯:
~~~
Db::execute('insert into data (name) values (:name),(:name)',['name'=>'thinkphp']);
~~~

該錯誤信息表示你的參數(shù)綁定參數(shù)數(shù)量不符。
## 查詢返回值
使用`Db`類查詢數(shù)據(jù)庫的話,`query`方法的返回值是一個二維數(shù)組的數(shù)據(jù)集,每個元素就是一條記錄,例如:
~~~
array (size=1)
0 =>
array (size=5)
'id' => int 8
'name' => string 'thinkphp' (length=8)
~~~
>[danger] 如果沒有查詢到任何數(shù)據(jù),返回值就是一個空數(shù)組。
相比`query`方法,`execute`方法的返回值就比較單純,一般就是返回影響(包括新增和更新)的記錄數(shù),如果沒有影響任何記錄,則返回值為`0`,所以千萬不要用布爾值來判斷`execute`是否執(zhí)行成功,事實上,在`5.0`里面不需要判斷是否成功,因為如果發(fā)生錯誤一定會拋出異常。
## 動態(tài)連接數(shù)據(jù)庫
當你需要使用多個數(shù)據(jù)庫連接的時候,就需要使用`connect`方法動態(tài)切換到另外一個數(shù)據(jù)庫連接,假設存在另外一個數(shù)據(jù)庫`test`,并且復制`data`過去更名為`test`,然后測試下面的示例:
~~~
Db::query('select * from data where id = 2');
Db::connect([
// 數(shù)據(jù)庫類型
'type' => 'mysql',
// 服務器地址
'hostname' => '127.0.0.1',
// 數(shù)據(jù)庫名
'database' => 'test',
// 用戶名
'username' => 'root',
// 密碼
'password' => '',
// 開啟調(diào)試模式
'debug' => true,
])->query('select * from test where id = 1');
Db::query('select * from data where id = 3');
~~~
頁面Trace的顯示信息可以看出來使用了兩次數(shù)據(jù)庫連接和執(zhí)行了三次查詢,并且數(shù)據(jù)庫連接切換并沒有影響默認的查詢(第三個查詢還是使用的默認數(shù)據(jù)庫配置連接,`test`數(shù)據(jù)庫中并不存在`data`表,如果連接的還是第二個數(shù)據(jù)庫連接的話肯定會報錯)。

有時候,我們只需要設置一些基本的數(shù)據(jù)庫配置參數(shù),可以簡化成一個字符串格式定義(該格式為ThinkPHP使用規(guī)范,而不是PDO連接規(guī)范,不要和`DSN`混淆起來):
~~~
Db::connect('mysql://root@127.0.0.1/demo#utf8')
->query('select * from data where id = 1');
~~~
字符串格式的連接信息規(guī)范格式如下:
>[info]#### 數(shù)據(jù)庫類型://用戶名[:用戶密碼]@數(shù)據(jù)庫服務器地址[:端口]/數(shù)據(jù)庫名[?參數(shù)1=值&參數(shù)2=值]#數(shù)據(jù)庫編碼
`Db`類的`connect`方法會返回一個數(shù)據(jù)庫連接對象實例,相同的連接參數(shù)返回的是同一個對象實例,除非你強制重新實例化,例如:
~~~
Db::connect([
// 數(shù)據(jù)庫類型
'type' => 'mysql',
// 服務器地址
'hostname' => '127.0.0.1',
// 數(shù)據(jù)庫名
'database' => 'demo',
// 用戶名
'username' => 'root',
// 密碼
'password' => '',
],true)->query('select * from data where id = 1');
~~~
這樣,每次調(diào)用都會重新實例化數(shù)據(jù)庫的連接類。
為了便于統(tǒng)一管理,你可以把數(shù)據(jù)庫配置參數(shù)納入配置文件,例如在應用配置文件中添加:
~~~
'db_config' => [
// 數(shù)據(jù)庫類型
'type' => 'mysql',
// 服務器地址
'hostname' => '127.0.0.1',
// 數(shù)據(jù)庫名
'database' => 'demo',
// 用戶名
'username' => 'root',
// 密碼
'password' => '',
],
~~~
或者使用字符串方式定義
~~~
'db_config' => 'mysql://root@127.0.0.1/demo',
~~~
>[danger] 上面的`db_config`配置參數(shù)不是在數(shù)據(jù)庫配置文件中定義,而是在應用配置文件或者模塊配置文件中定義。
然后,使用下面的方式來動態(tài)連接獲取切換連接
~~~
Db::connect('db_config')
->query('select * from data where id=:id', ['id'=>3]);
~~~
當`connect`方法傳入的連接參數(shù)是字符串并且不包含`/`等特殊符號的話,表示使用的是預定義數(shù)據(jù)庫配置參數(shù)。
## 分布式支持
數(shù)據(jù)訪問層支持分布式數(shù)據(jù)庫,包括讀寫分離,要啟用分布式數(shù)據(jù)庫,需要開啟數(shù)據(jù)庫配置文件中的`deploy`參數(shù):
~~~
return [
// 啟用分布式數(shù)據(jù)庫
'deploy' => 1,
// 數(shù)據(jù)庫類型
'type' => 'mysql',
// 服務器地址
'hostname' => '192.168.1.1,192.168.1.2',
// 數(shù)據(jù)庫名
'database' => 'demo',
// 數(shù)據(jù)庫用戶名
'username' => 'root',
// 數(shù)據(jù)庫密碼
'password' => '',
// 數(shù)據(jù)庫連接端口
'hostport' => '',
];
~~~
啟用分布式數(shù)據(jù)庫后,`hostname`參數(shù)是關鍵,`hostname`的個數(shù)決定了分布式數(shù)據(jù)庫的數(shù)量,默認情況下第一個地址就是主服務器。
主從服務器支持設置不同的連接參數(shù),包括:
|連接參數(shù)|
|---|
|username|
|password|
|hostport|
|database|
|dsn|
|charset|
如果主從服務器的上述參數(shù)一致的話,只需要設置一個,對于不同的參數(shù),可以分別設置,例如:
~~~
return [
// 啟用分布式數(shù)據(jù)庫
'deploy' => 1,
// 數(shù)據(jù)庫類型
'type' => 'mysql',
// 服務器地址
'hostname' => '192.168.1.1,192.168.1.2,192.168.1.3',
// 數(shù)據(jù)庫名
'database' => 'demo',
// 數(shù)據(jù)庫用戶名
'username' => 'root,slave,slave',
// 數(shù)據(jù)庫密碼
'password' => '123456',
// 數(shù)據(jù)庫連接端口
'hostport' => '',
// 數(shù)據(jù)庫字符集
'charset' => 'utf8',
];
~~~
>記住,要么相同,要么每個都要設置。
還可以設置分布式數(shù)據(jù)庫的讀寫是否分離,默認的情況下讀寫不分離,也就是每臺服務器都可以進行讀寫操作,對于主從式數(shù)據(jù)庫而言,需要設置讀寫分離,通過下面的設置就可以:
~~~
'rw_separate' => true,
~~~
在讀寫分離的情況下,默認第一個數(shù)據(jù)庫配置是主服務器的配置信息,負責寫入數(shù)據(jù),如果設置了`master_num`參數(shù),則可以支持多個主服務器寫入(每次隨機連接其中一個主服務器)。其它的地址都是從數(shù)據(jù)庫,負責讀取數(shù)據(jù),數(shù)量不限制。每次連接從服務器并且進行讀取操作的時候,系統(tǒng)會隨機進行在從服務器中選擇。同一個數(shù)據(jù)庫連接的每次請求只會連接一次主服務器和從服務器,如果某次請求的從服務器連接不上,會自動切換到主服務器進行查詢操作。
如果不希望隨機讀取,或者某種情況下其它從服務器暫時不可用,還可以設置`slave_no` 指定固定服務器進行讀操作,`slave_no`指定的序號表示`hostname`中數(shù)據(jù)庫地址的序號,從`0`開始。
調(diào)用查詢類或者模型的`CURD`操作的話,系統(tǒng)會自動判斷當前執(zhí)行的方法是讀操作還是寫操作并自動連接主從服務器,如果你用的是原生SQL,那么需要注意系統(tǒng)的默認規(guī)則: 寫操作必須用數(shù)據(jù)庫的`execute`方法,讀操作必須用數(shù)據(jù)庫的`query`方法,否則會發(fā)生主從讀寫錯亂的情況。
發(fā)生下列情況的話,會自動連接主服務器:
* 使用了數(shù)據(jù)庫的寫操作方法(`execute`/`insert`/`update`/`delete`以及衍生方法);
* 如果調(diào)用了數(shù)據(jù)庫事務方法的話,會自動連接主服務器;
* 從服務器連接失敗,會自動連接主服務器;
* 調(diào)用了查詢構造器的`lock`方法;
* 調(diào)用了查詢構造器的`master`方法
>[danger] 主從數(shù)據(jù)庫的數(shù)據(jù)同步工作不在框架實現(xiàn),需要數(shù)據(jù)庫考慮自身的同步或者復制機制。如果在大數(shù)據(jù)量或者特殊的情況下寫入數(shù)據(jù)后可能會存在同步延遲的情況,可以調(diào)用`master()`方法進行主庫查詢操作。
>[info] 在實際生產(chǎn)環(huán)境中,很多云主機的數(shù)據(jù)庫分布式實現(xiàn)機制和本地開發(fā)會有所區(qū)別,但通常會采下面用兩種方式:
>
> * 第一種:提供了寫IP和讀IP(一般是虛擬IP),進行數(shù)據(jù)庫的讀寫分離操作;
> * 第二種:始終保持同一個IP連接數(shù)據(jù)庫,內(nèi)部會進行讀寫分離IP調(diào)度(阿里云就是采用該方式)。
## 存儲過程調(diào)用
數(shù)據(jù)訪問層支持存儲過程調(diào)用,調(diào)用數(shù)據(jù)庫存儲過程使用下面的方法:
~~~
$resultSet = Db::query('call procedure_name');
foreach ($resultSet as $result) {
}
~~~
存儲過程返回的是一個數(shù)據(jù)集,如果你的存儲過程不需要返回任何的數(shù)據(jù),那么也可以使用`execute`方法:
~~~
Db::execute('call procedure_name');
~~~
存儲過程可以支持輸入和輸出參數(shù),以及進行參數(shù)綁定操作。
~~~
$resultSet = Db::query('call procedure_name(:in_param1,:in_param2,:out_param)', [
'in_param1' => $param1,
'in_param2' => [$param2, PDO::PARAM_INT],
'out_param' => [$outParam, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 4000],
]);
~~~
輸出參數(shù)的綁定必須額外使用`PDO::PARAM_INPUT_OUTPUT`,并且可以和輸入?yún)?shù)公用一個參數(shù)。
>[danger] 無論存儲過程內(nèi)部做了什么操作,每次存儲過程調(diào)用僅僅被當成一次查詢。
## 數(shù)據(jù)庫事務
5.0對數(shù)據(jù)庫事務的封裝更為完善,事務的支持由連接器類來完成,但查詢器類中也對事務進行了封裝調(diào)用,不過我們?nèi)匀恢恍枰ㄟ^Db類便可完成事務操作。
>[danger] 使用事務處理的話,需要數(shù)據(jù)庫引擎支持事務處理。比如`MySQL`的`MyISAM`類型不支持事務處理,需要使用`InnoDB`引擎。
最簡單的方法是使用`transaction`方法操作數(shù)據(jù)庫事務,會自動控制事務處理,當發(fā)生任何異常會自動回滾,例如:
~~~
Db::transaction(function () {
Db::table('user')->find(1);
Db::table('user')->where('id', 1)->save(['name' => 'thinkphp']);
Db::table('user')->delete(1);
});
~~~
也可以手動控制事務,例如:
~~~
// 啟動事務
Db::startTrans();
try{
Db::table('user')->find(1);
Db::table('user')->where('id',1)->save(['name'=>'thinkphp']);
Db::table('user')->delete(1);
// 提交事務
Db::commit();
} catch (\Exception $e) {
// 回滾事務
Db::rollback();
}
~~~
>[danger] 在事務操作的時候,確保你的數(shù)據(jù)庫連接是同一個,否則事務會失效,`V5.0.9`版本之前的`db`助手函數(shù)都是默認重新鏈接數(shù)據(jù)庫的,請不要在事務中使用。
## 總結
通過本章的學習,你應該了解了5.0的數(shù)據(jù)庫架構設計和數(shù)據(jù)庫抽象訪問層的組成,以及如何配置數(shù)據(jù)庫信息和使用基礎的原生查詢,掌握了用`Db`類的`connect`方法切換不同的數(shù)據(jù)庫連接,基本了解了存儲過程及事務的用法。后面一章,我們會先來了解下數(shù)據(jù)庫的創(chuàng)建和數(shù)據(jù)遷移,之后就會進入真正的數(shù)據(jù)庫查詢的學習了。