标题:【Laravel系列4.1】连接数据库与原生查询
连接数据库与原生查询
在 PHP 的学习中,数据库,也就是 MySQL 就像它的亲兄弟一样,永远没法分家。同理,在框架中,数据库相关的功能也是所有框架必备的内容。从最早期我们会自己封装一个 MyDB 这种的数据库操作文件,到框架提供一套完整的 CRUD 类,再到现代化的框架中的 ORM ,其基础都是在变着花样的完成数据操作。当然,本身数据库也是 WEB 开发中的核心,所以一个框架对于数据库的支持的好坏,也会影响到它的普及。
Laravel 框架中的 DB 和 ORM 是两个不同的组件,关于 ORM 的概念,我们也将在相关的学习中了解到,但是现在我们先从简单的普通查询学起。今天的内容比较简单,我们要先能连接数据库,然后再能使用原始 SQL 语句的方式来对数据进行操作。
连接数据库配置
首先我们可以看下配置文件,在 Laravel 程序的 config 目录下,有一个 database.php 文件,其中有关于数据库的连接配置信息。
// ………………
// ………………
'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'),
]) : [],
],
// ………………
// ………………
在这个配置文件中,我们还能看到许多其它数据库的配置,不过,今天我们的重点还是在 mysql 这个配置中。除了这个默认配置外,我们还可以再添加多个连接配置,只要复制这个 mysql 的配置,然后改名就可以了。从 options 这个参数里面,我们可以看出,Laravel 默认使用的是 PDO 连接的数据库,我也没有研究在 Laravel 中如何使用 mysqli 进行连接,因为 PDO 确实已经是事实的连库标准了,完全没必要另辟蹊径。
在这个 mysql 的配置中,我们会发现很多 env() 函数调用的信息。这个函数是用于读取 .env 文件中所写的配置信息的。它有两个参数,一个是指定的配置文件中的键名,一个是如果没有找到的话,就会给一个默认值。关于这个函数,还记得我们在之前就已经讲过了。比如现在在我的本地测试环境中,连接数据库就是使用 .env 中如下的配置:
// ………………
// ………………
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
// ………………
// ………………
我的本地数据库不需要密码,连接也不需要做其它的操作,所以可以非常简单地这样配置一下就可以了。这样,线上、测试和本地环境,就不会互相冲突,也不需要我们在各个环境中进行各种 hosts 修改。
原生查询
接下来,我们就学习怎么使用原生 SQL 语句进行数据库操作。这种操作其实就像是 Laravel 为我们封装好了 PDO 的调用,也就是像我们在很早前自己封装的那种数据库调用类一样,非常简单方便。不过首先,我们要建立一张测试表,之后我们将对这张表进行 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;
目前这个表是没有数据的,所以我们需要先添加几条数据。
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/>';
}
});
因为是测试数据库的操作,所以就直接在路由中写代码了,在实际的业务开发中,大家可不要这么做哦。在代码中,我们通过 DB 这个门面类的 insert() 方法,就可以实现原生语句的增加操作。对于路由来说,其实我们不用写完全限定命名空间的类名,直接写个 DB 也是可以的。不过在这里为了突显出我们是调用了这个门面类,所以才写了这个完全限定名字称的类名。
看这个 insert() 函数的参数写法,是不是和 PDO 的预处理语句的写法很像?语句里面使用占位符,后面一个数组里面传递参数。没错,前面也说过,本身 Laravel 的数据库操作就是使用的 PDO 的,不记得的小伙伴可以移步 【PHP中的PDO操作学习(四)查询结构集】https://mp.weixin.qq.com/s/dv-lnEGV0JlGsjy4rl_jkw 查看 PDO 相关的基础知识。我们也可以使用 :xxx 这样的占位符,这个大家自己去试下吧。
注意,insert() 方法返回的结果是一个布尔值,也就是添加操作的成功失败情况,如果我们想获取新增加的数据的 id ,需要使用 DB::getPdo()->lastInsertId(); 这条语句才可以获取到。
做完新增了,我们再来试下修改和删除。
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 '参数错误';
}
\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 '参数错误';
}
\Illuminate\Support\Facades\DB::delete('delete from raw_test where id = :id', ['id'=>$id]);
echo '删除成功';
});
代码很简单,就不多做解释了,不过这里大家能看到的一点是,我们在修改和删除操作中,绑定数据使用的是 :xxx 这种方式哦!
在学习 PDO 的时候,我们知道,预处理语句的执行就是先 prepare() 再 execute() 一下就可以了,特别是增删改的操作是非常类似的,那么我们在这里是不是可以在 insert() 方法里面执行一个修改或者删除语句呢?我们先尝试一下。
Route::get('rawdb/test/delete2', function () {
$id = request()->id;
if($id < 1){
echo '参数错误';
}
\Illuminate\Support\Facades\DB::insert('delete from raw_test where id = :id', ['id'=>$id]);
echo '删除成功';
});
嗯,你猜对了,我们的执行成功了,使用 insert() 方法,但是里面的语句是一条 delete 语句,是可以执行成功的。这就很诡异了吧,为什么要这样呢?直接提供一个方法让我们进行操作就好了嘛。其实,这也正是 Laravel 优雅的由来。为了更好地区分度和代码的清晰。我们在审阅查看代码时,按照标准的规范写,不需要详细的看语句,就可以通过方法名快速地知道这段数据库操作是要干什么,这不是非常好的一件事嘛。
在 laravel/framework/src/Illuminate/Database/Connection.php 文件中,我们可以找到 insert() 、update()、delete() 这些方法,其中 insert() 会继续调用一个 statement() 方法,而 update() 和 delete() 会调用 affectingStatement() 方法。仔细查看这两个方法,你会发现只有返回结果的地方是稍有不同的,statement() 返回的是布尔值,而 affectingStatement() 返回的是影响行数。
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;
});
}
看这个源代码,是不是马上就能看出来,$statment 就是一个 PDO 的预编译对象 PDOStatment ,后面的操作其实就是 PDO 的操作了。
好了,最后还差一个查询,查询就更简单了,我们直接测试一下下面的代码就好了。查阅的源代码也在上面的那个文件中哦,大家可以自己去看一看,内容和上面的那两个 statment 方法里面的东西都差不多,也是在返回结果的地方会有些区别。
Route::get('rawdb/test/show', function () {
dd(\Illuminate\Support\Facades\DB::select("select * from raw_test"));
});
dd() 这个方法貌似前面一直没讲过,它是一个可以方便快速调试的函数。大家试试就知道啦!
连接另外一个数据库
上面通过使用原生语句的方式我们可以方便地进行增、删、改、查操作了,也就是常说的 CRUD 。接下来我们来看看怎样连接其它的数据库。
首先,我们新建一个数据库,就叫 laravel8 好了,并且同样的建立一个 raw_test 表,然后就是在 .env 中配置这个数据库的连接信息。
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=
其实就是复制了一下基础的那个 DB 配置,然后改了下配置名称以及连接的数据库名称。接下来,修改 config/database.php 文件,增加一个连接配置。
'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'),
]) : [],
],
同样的,我们也是复制了一下 mysql 那个配置,然后修改相关的名称以及 env() 读取字段的名称。通过上面两步,我们的配置就完成了,是不是非常简单,接下来就是在代码中如何使用。
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"));
});
注意看代码,其实我们只是多使用了一个 connection() 方法。它的作用就是找到指定的连接,在默认情况下,Laravel 框架会去找 mysql 这个配置,如果我们需要操作其它数据库的话,就需要通过 connection() 来指定要连接的数据库。是不是非常简单明了,配置过程也很轻松方便。
底层的 PDO 在哪里?
在使用 DB 门面的情况下,我们会通过服务容器注册门面并实例化一个 laravel/framework/src/Illuminate/Database/DatabaseManager.php 对象,它的 connection() 方法会读取配置信息,然后通过 makeConnection() 方法去创建连接。
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 了吧,能起这个名字的基本上多少都会和工厂沾边,不过在这里,它指向的是一个明确的 laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php 对象。通过 ConnectionFactory 里面的 make() 方法,根据情况调用不同的连接创建方法,一路向下,我们进入到 createSingleConnection() 方法中,继续前进,进入到 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']}].");
}
注意这个 createConnector() 方法,它是一个 简单工厂 模式的应用,通过它,我们获得了配置文件中相关配置的连接对象,比如 mysql 数据库的返回的就是 MySqlConnector 这个对象。接下来,调用它的 connect() 方法,这时我们会进入 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() 方法,在这个方法中会继续调用 createPdoConnection() 这个方法。
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() 见到了 PDO 的真容,这一路走来真的是跋山涉水呀!不过,总算我们还是不负所望地找到了 PDO 到底是在哪里创建的。在这其中,我们还看到了 工厂模式 在这其中发挥的作用。也算是取到了一部分的真经,大家都要为自己鼓掌哦!
总结
数据库上手就是一堆源码,不过这也让我们搞清楚了 Laravel 在底层是如何去创建一个 PDO 对象的。而且我们会发现,Laravel 只能使用 PDO ,无法使用 MySQLi 来进行数据库操作。当然,这也是为了框架的通用性,因为 PDO 也是通用的,在工厂中,我们可以看到 Postgres、SQLite、SQLServer 的连接器,如果使用 MySQLi 的话,可就没办法支持这些数据库了哦。
参考文档:
https://learnku.com/docs/laravel/8.x/database/9400