在 PHP 的學(xué)習(xí)中,數(shù)據(jù)庫(kù),也就是 MySQL 就像它的親兄弟一樣,永遠(yuǎn)沒(méi)法分家。同理,在框架中,數(shù)據(jù)庫(kù)相關(guān)的功能也是所有框架必備的內(nèi)容。從最早期我們會(huì)自己封裝一個(gè) MyDB 這種的數(shù)據(jù)庫(kù)操作文件,到框架提供一套完整的 CRUD 類,再到現(xiàn)代化的框架中的 ORM ,其基礎(chǔ)都是在變著花樣的完成數(shù)據(jù)操作。當(dāng)然,本身數(shù)據(jù)庫(kù)也是 WEB 開(kāi)發(fā)中的核心,所以一個(gè)框架對(duì)于數(shù)據(jù)庫(kù)的支持的好壞,也會(huì)影響到它的普及。
Laravel 框架中的 DB 和 ORM 是兩個(gè)不同的組件,關(guān)于 ORM 的概念,我們也將在相關(guān)的學(xué)習(xí)中了解到,但是現(xiàn)在我們先從簡(jiǎn)單的普通查詢學(xué)起。今天的內(nèi)容比較簡(jiǎn)單,我們要先能連接數(shù)據(jù)庫(kù),然后再能使用原始 SQL 語(yǔ)句的方式來(lái)對(duì)數(shù)據(jù)進(jìn)行操作。
首先我們可以看下配置文件,在 Laravel 程序的 config 目錄下,有一個(gè) database.php 文件,其中有關(guān)于數(shù)據(jù)庫(kù)的連接配置信息。
// ………………
// ………………
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
// ………………
// ………………
在這個(gè)配置文件中,我們還能看到許多其它數(shù)據(jù)庫(kù)的配置,不過(guò),今天我們的重點(diǎn)還是在 mysql 這個(gè)配置中。除了這個(gè)默認(rèn)配置外,我們還可以再添加多個(gè)連接配置,只要復(fù)制這個(gè) mysql 的配置,然后改名就可以了。從 options 這個(gè)參數(shù)里面,我們可以看出,Laravel 默認(rèn)使用的是 PDO 連接的數(shù)據(jù)庫(kù),我也沒(méi)有研究在 Laravel 中如何使用 mysqli 進(jìn)行連接,因?yàn)?PDO 確實(shí)已經(jīng)是事實(shí)的連庫(kù)標(biāo)準(zhǔn)了,完全沒(méi)必要另辟蹊徑。
在這個(gè) mysql 的配置中,我們會(huì)發(fā)現(xiàn)很多 env() 函數(shù)調(diào)用的信息。這個(gè)函數(shù)是用于讀取 .env 文件中所寫的配置信息的。它有兩個(gè)參數(shù),一個(gè)是指定的配置文件中的鍵名,一個(gè)是如果沒(méi)有找到的話,就會(huì)給一個(gè)默認(rèn)值。關(guān)于這個(gè)函數(shù),還記得我們?cè)谥熬鸵呀?jīng)講過(guò)了。比如現(xiàn)在在我的本地測(cè)試環(huán)境中,連接數(shù)據(jù)庫(kù)就是使用 .env 中如下的配置:
// ………………
// ………………
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
// ………………
// ………………
我的本地?cái)?shù)據(jù)庫(kù)不需要密碼,連接也不需要做其它的操作,所以可以非常簡(jiǎn)單地這樣配置一下就可以了。這樣,線上、測(cè)試和本地環(huán)境,就不會(huì)互相沖突,也不需要我們?cè)诟鱾€(gè)環(huán)境中進(jìn)行各種 hosts 修改。
接下來(lái),我們就學(xué)習(xí)怎么使用原生 SQL 語(yǔ)句進(jìn)行數(shù)據(jù)庫(kù)操作。這種操作其實(shí)就像是 Laravel 為我們封裝好了 PDO 的調(diào)用,也就是像我們?cè)诤茉缜白约悍庋b的那種數(shù)據(jù)庫(kù)調(diào)用類一樣,非常簡(jiǎn)單方便。不過(guò)首先,我們要建立一張測(cè)試表,之后我們將對(duì)這張表進(jìn)行 CRUD 操作。
CREATE TABLE `raw_test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
`sex` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
目前這個(gè)表是沒(méi)有數(shù)據(jù)的,所以我們需要先添加幾條數(shù)據(jù)。
Route::get('rawdb/test/insert', function () {
$data = [
'Peter' => 1,
'Tom' => 1,
'Susan' => 2,
'Mary' => 2,
'Jim' => 1,
];
foreach ($data as $k => $v) {
\Illuminate\Support\Facades\DB::insert('insert into raw_test (name, sex) values (?, ?)', [$k, $v]);
$insertId = DB::getPdo()->lastInsertId();
echo $insertId, '<br/>';
}
});
因?yàn)槭菧y(cè)試數(shù)據(jù)庫(kù)的操作,所以就直接在路由中寫代碼了,在實(shí)際的業(yè)務(wù)開(kāi)發(fā)中,大家可不要這么做哦。在代碼中,我們通過(guò) DB 這個(gè)門面類的 insert() 方法,就可以實(shí)現(xiàn)原生語(yǔ)句的增加操作。對(duì)于路由來(lái)說(shuō),其實(shí)我們不用寫完全限定命名空間的類名,直接寫個(gè) DB 也是可以的。不過(guò)在這里為了突顯出我們是調(diào)用了這個(gè)門面類,所以才寫了這個(gè)完全限定名字稱的類名。
看這個(gè) insert() 函數(shù)的參數(shù)寫法,是不是和 PDO 的預(yù)處理語(yǔ)句的寫法很像?語(yǔ)句里面使用占位符,后面一個(gè)數(shù)組里面?zhèn)鬟f參數(shù)。沒(méi)錯(cuò),前面也說(shuō)過(guò),本身 Laravel 的數(shù)據(jù)庫(kù)操作就是使用的 PDO 的,不記得的小伙伴可以移步 【PHP中的PDO操作學(xué)習(xí)(四)查詢結(jié)構(gòu)集】https://mp.weixin.qq.com/s/dv-lnEGV0JlGsjy4rl_jkw 查看 PDO 相關(guān)的基礎(chǔ)知識(shí)。我們也可以使用 :xxx 這樣的占位符,這個(gè)大家自己去試下吧。
注意,insert() 方法返回的結(jié)果是一個(gè)布爾值,也就是添加操作的成功失敗情況,如果我們想獲取新增加的數(shù)據(jù)的 id ,需要使用 DB::getPdo()->lastInsertId(); 這條語(yǔ)句才可以獲取到。
做完新增了,我們?cè)賮?lái)試下修改和刪除。
Route::get('rawdb/test/update', function () {
$data = [
'name' => request()->name,
'sex' => request()->sex,
'id' => request()->id
];
if($data['id'] < 1 || !$data['name'] || !in_array($data['sex'], [1, 2])){
echo '參數(shù)錯(cuò)誤';
}
\Illuminate\Support\Facades\DB::update('update raw_test set name=:name,sex =:sex where id = :id', $data);
echo '修改成功';
});
Route::get('rawdb/test/delete', function () {
$id = request()->id;
if($id < 1){
echo '參數(shù)錯(cuò)誤';
}
\Illuminate\Support\Facades\DB::delete('delete from raw_test where id = :id', ['id'=>$id]);
echo '刪除成功';
});
代碼很簡(jiǎn)單,就不多做解釋了,不過(guò)這里大家能看到的一點(diǎn)是,我們?cè)谛薷暮蛣h除操作中,綁定數(shù)據(jù)使用的是 :xxx 這種方式哦!
在學(xué)習(xí) PDO 的時(shí)候,我們知道,預(yù)處理語(yǔ)句的執(zhí)行就是先 prepare() 再 execute() 一下就可以了,特別是增刪改的操作是非常類似的,那么我們?cè)谶@里是不是可以在 insert() 方法里面執(zhí)行一個(gè)修改或者刪除語(yǔ)句呢?我們先嘗試一下。
Route::get('rawdb/test/delete2', function () {
$id = request()->id;
if($id < 1){
echo '參數(shù)錯(cuò)誤';
}
\Illuminate\Support\Facades\DB::insert('delete from raw_test where id = :id', ['id'=>$id]);
echo '刪除成功';
});
嗯,你猜對(duì)了,我們的執(zhí)行成功了,使用 insert() 方法,但是里面的語(yǔ)句是一條 delete 語(yǔ)句,是可以執(zhí)行成功的。這就很詭異了吧,為什么要這樣呢?直接提供一個(gè)方法讓我們進(jìn)行操作就好了嘛。其實(shí),這也正是 Laravel 優(yōu)雅的由來(lái)。為了更好地區(qū)分度和代碼的清晰。我們?cè)趯忛啿榭创a時(shí),按照標(biāo)準(zhǔn)的規(guī)范寫,不需要詳細(xì)的看語(yǔ)句,就可以通過(guò)方法名快速地知道這段數(shù)據(jù)庫(kù)操作是要干什么,這不是非常好的一件事嘛。
在 laravel/framework/src/Illuminate/Database/Connection.php 文件中,我們可以找到 insert() 、update()、delete() 這些方法,其中 insert() 會(huì)繼續(xù)調(diào)用一個(gè) statement() 方法,而 update() 和 delete() 會(huì)調(diào)用 affectingStatement() 方法。仔細(xì)查看這兩個(gè)方法,你會(huì)發(fā)現(xiàn)只有返回結(jié)果的地方是稍有不同的,statement() 返回的是布爾值,而 affectingStatement() 返回的是影響行數(shù)。
public function statement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return true;
}
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$this->recordsHaveBeenModified();
return $statement->execute();
});
}
public function affectingStatement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return 0;
}
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();
$this->recordsHaveBeenModified(
($count = $statement->rowCount()) > 0
);
return $count;
});
}
看這個(gè)源代碼,是不是馬上就能看出來(lái),$statment 就是一個(gè) PDO 的預(yù)編譯對(duì)象 PDOStatment ,后面的操作其實(shí)就是 PDO 的操作了。
好了,最后還差一個(gè)查詢,查詢就更簡(jiǎn)單了,我們直接測(cè)試一下下面的代碼就好了。查閱的源代碼也在上面的那個(gè)文件中哦,大家可以自己去看一看,內(nèi)容和上面的那兩個(gè) statment 方法里面的東西都差不多,也是在返回結(jié)果的地方會(huì)有些區(qū)別。
Route::get('rawdb/test/show', function () {
dd(\Illuminate\Support\Facades\DB::select("select * from raw_test"));
});
dd() 這個(gè)方法貌似前面一直沒(méi)講過(guò),它是一個(gè)可以方便快速調(diào)試的函數(shù)。大家試試就知道啦!
上面通過(guò)使用原生語(yǔ)句的方式我們可以方便地進(jìn)行增、刪、改、查操作了,也就是常說(shuō)的 CRUD 。接下來(lái)我們來(lái)看看怎樣連接其它的數(shù)據(jù)庫(kù)。
首先,我們新建一個(gè)數(shù)據(jù)庫(kù),就叫 laravel8 好了,并且同樣的建立一個(gè) raw_test 表,然后就是在 .env 中配置這個(gè)數(shù)據(jù)庫(kù)的連接信息。
DB_CONNECTION_LARAVEL8=mysql
DB_HOST_LARAVEL8=127.0.0.1
DB_PORT_LARAVEL8=3306
DB_DATABASE_LARAVEL8=laravel8
DB_USERNAME_LARAVEL8=root
DB_PASSWORD_LARAVEL8=
其實(shí)就是復(fù)制了一下基礎(chǔ)的那個(gè) DB 配置,然后改了下配置名稱以及連接的數(shù)據(jù)庫(kù)名稱。接下來(lái),修改 config/database.php 文件,增加一個(gè)連接配置。
'laravel8' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL_LARAVEL8'),
'host' => env('DB_HOST_LARAVEL8', '127.0.0.1'),
'port' => env('DB_PORT_LARAVEL8', '3306'),
'database' => env('DB_DATABASE_LARAVEL8', 'forge'),
'username' => env('DB_USERNAME_LARAVEL8', 'forge'),
'password' => env('DB_PASSWORD_LARAVEL8', ''),
'unix_socket' => env('DB_SOCKET_LARAVEL8', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
同樣的,我們也是復(fù)制了一下 mysql 那個(gè)配置,然后修改相關(guān)的名稱以及 env() 讀取字段的名稱。通過(guò)上面兩步,我們的配置就完成了,是不是非常簡(jiǎn)單,接下來(lái)就是在代碼中如何使用。
Route::get('rawdb/laravel8/test', function () {
\Illuminate\Support\Facades\DB::connection('laravel8')->insert('insert into raw_test (name, sex) values (?, ?)', ['Sam', 1]);
dd(\Illuminate\Support\Facades\DB::connection('laravel8')->select("select * from raw_test"));
});
注意看代碼,其實(shí)我們只是多使用了一個(gè) connection() 方法。它的作用就是找到指定的連接,在默認(rèn)情況下,Laravel 框架會(huì)去找 mysql 這個(gè)配置,如果我們需要操作其它數(shù)據(jù)庫(kù)的話,就需要通過(guò) connection() 來(lái)指定要連接的數(shù)據(jù)庫(kù)。是不是非常簡(jiǎn)單明了,配置過(guò)程也很輕松方便。
在使用 DB 門面的情況下,我們會(huì)通過(guò)服務(wù)容器注冊(cè)門面并實(shí)例化一個(gè) laravel/framework/src/Illuminate/Database/DatabaseManager.php 對(duì)象,它的 connection() 方法會(huì)讀取配置信息,然后通過(guò) makeConnection() 方法去創(chuàng)建連接。
protected function makeConnection($name)
{
$config = $this->configuration($name);
if (isset($this->extensions[$name])) {
return call_user_func($this->extensions[$name], $config, $name);
}
if (isset($this->extensions[$driver = $config['driver']])) {
return call_user_func($this->extensions[$driver], $config, $name);
}
return $this->factory->make($config, $name);
}
看到 factory 了吧,能起這個(gè)名字的基本上多少都會(huì)和工廠沾邊,不過(guò)在這里,它指向的是一個(gè)明確的 laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php 對(duì)象。通過(guò) ConnectionFactory 里面的 make() 方法,根據(jù)情況調(diào)用不同的連接創(chuàng)建方法,一路向下,我們進(jìn)入到 createSingleConnection() 方法中,繼續(xù)前進(jìn),進(jìn)入到 createPdoResolverWithHosts() 方法。
protected function createPdoResolverWithHosts(array $config)
{
return function () use ($config) {
foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
} catch (PDOException $e) {
continue;
}
}
throw $e;
};
}
public function createConnector(array $config)
{
if (! isset($config['driver'])) {
throw new InvalidArgumentException('A driver must be specified.');
}
if ($this->container->bound($key = "db.connector.{$config['driver']}")) {
return $this->container->make($key);
}
switch ($config['driver']) {
case 'mysql':
return new MySqlConnector;
case 'pgsql':
return new PostgresConnector;
case 'sqlite':
return new SQLiteConnector;
case 'sqlsrv':
return new SqlServerConnector;
}
throw new InvalidArgumentException("Unsupported driver [{$config['driver']}].");
}
注意這個(gè) createConnector() 方法,它是一個(gè) 簡(jiǎn)單工廠 模式的應(yīng)用,通過(guò)它,我們獲得了配置文件中相關(guān)配置的連接對(duì)象,比如 mysql 數(shù)據(jù)庫(kù)的返回的就是 MySqlConnector 這個(gè)對(duì)象。接下來(lái),調(diào)用它的 connect() 方法,這時(shí)我們會(huì)進(jìn)入 laravel/framework/src/Illuminate/Database/Connectors/MySqlConnector.php 文件。
public function connect(array $config)
{
$dsn = $this->getDsn($config);
$options = $this->getOptions($config);
$connection = $this->createConnection($dsn, $config, $options);
if (! empty($config['database'])) {
$connection->exec("use `{$config['database']}`;");
}
$this->configureIsolationLevel($connection, $config);
$this->configureEncoding($connection, $config);
$this->setModes($connection, $config);
return $connection;
}
在這里,我們需要注意的是 createConnection() 方法,在這個(gè)方法中會(huì)繼續(xù)調(diào)用 createPdoConnection() 這個(gè)方法。
protected function createPdoConnection($dsn, $username, $password, $options)
{
if (class_exists(PDOConnection::class) && ! $this->isPersistentConnection($options)) {
return new PDOConnection($dsn, $username, $password, $options);
}
return new PDO($dsn, $username, $password, $options);
}
Oh,My God!我們總算在 createPdoConnection() 見(jiàn)到了 PDO 的真容,這一路走來(lái)真的是跋山涉水呀!不過(guò),總算我們還是不負(fù)所望地找到了 PDO 到底是在哪里創(chuàng)建的。在這其中,我們還看到了 工廠模式 在這其中發(fā)揮的作用。也算是取到了一部分的真經(jīng),大家都要為自己鼓掌哦!
數(shù)據(jù)庫(kù)上手就是一堆源碼,不過(guò)這也讓我們搞清楚了 Laravel 在底層是如何去創(chuàng)建一個(gè) PDO 對(duì)象的。而且我們會(huì)發(fā)現(xiàn),Laravel 只能使用 PDO ,無(wú)法使用 MySQLi 來(lái)進(jìn)行數(shù)據(jù)庫(kù)操作。當(dāng)然,這也是為了框架的通用性,因?yàn)?PDO 也是通用的,在工廠中,我們可以看到 Postgres、SQLite、SQLServer 的連接器,如果使用 MySQLi 的話,可就沒(méi)辦法支持這些數(shù)據(jù)庫(kù)了哦。
參考文檔:
https://learnku.com/docs/laravel/8.x/database/9400
聯(lián)系客服