事务以及PDO属性设置
今天学习的内容比较轻松,就讲两个小东西,而且也没什么特别的源码方面的内容。主要也是因为这两个小功能的应用会比较广泛,并且源码实现也非常简单易懂,我就简单的说一下源码大概的位置,大家直接自己看一下就好了。因此,这篇文章也可以看成是本系列教程学习的一个中场休息。
事务
对于数据库来说,事务操作是非常经典而且也很实用的一个技术。具体事务是干什么的我们就不多说了,毕竟这也不是数据库知识普及的文章。在电商、金融类应用中,事务是非常重要的功能,也是必须的能力。在 Laravel 中操作事务可以说是简单到没朋友。
代码语言:javascript复制Route::get('db/tran/insert', function(){
IlluminateSupportFacadesDB::beginTransaction();
try {
IlluminateSupportFacadesDB::table('db_test')->insert(['name' => 'Lily', 'sex' => 2]);
IlluminateSupportFacadesDB::table('db_test_no')->insert(['name' => 'Lily', 'sex' => 2]);
IlluminateSupportFacadesDB::commit();
}catch(Exception $e){
IlluminateSupportFacadesDB::rollBack();
dd($e->getMessage());
}
});
我们还是非常简单的在路由中进行操作。通过 beginTransaction() 方法可以可以打开事务操作。在 try 里面,我特意将第二个语句的表名写错了,这样就会进入到 catch 中调用回滚的 rollBack() 方法。接下来我们找到 beginTransaction() 的实现方法,就是在 laravel/framework/src/Illuminate/Database/Connection.php 类所引用的 laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php 特性中。包括 rollBack() 以及 commit() 等方法的实现都在这里,大家自己看看源码,其实就是 PDO 的一套事务调用的封装。如果您已经忘了我们之前学习过的 【PHP中的PDO操作学习(二)预处理语句及事务】https://mp.weixin.qq.com/s/HswwtL6YEXW_4BwMV5RJ2w ,那么就赶紧回去看看吧!
PDO 属性设置
来填坑了,在【Laravel系列4.2:查询构造器】https://mp.weixin.qq.com/s/vUImsLTpEtELgdCTWI6k2A中,我们说过一个问题,那就是查询构造器查询出来的结果都是 对象 ,而且是一个 stdClass 对象。之前在学习 PDO 的时候,我们清楚地知道这是 PDO::ATTR_DEFAULT_FETCH_MODE 被设置成了 PDO::FETCH_OBJ 的结果,那么在 Laravel 框架中,我们如何修改这个配置呢?首先还是从 config/database.php 这个配置文件看起。在配置连接信息的时候,我们可以在 options 中设置一些 PDO 的默认属性。
代码语言:javascript复制'mysql3' => [
'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'),
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]) : [],
],
新添加的这个配置增加了 PDO::ATTR_DEFAULT_FETCH_MODE 并设置为 PDO::FETCH_ASSOC 。然后我们建一个路由来测试一下。
代码语言:javascript复制Route::get('db/collection/list', function(){
dump( IlluminateSupportFacadesDB::connection('mysql3')->table('db_test')->get()->toArray());
dump(IlluminateSupportFacadesDB::connection('mysql3')->getPdo());
// attributes: {
// CASE: NATURAL
// ERRMODE: EXCEPTION
// AUTOCOMMIT: 1
// PERSISTENT: false
// DRIVER_NAME: "mysql"
// SERVER_INFO: "Uptime: 266035 Threads: 5 Questions: 635 Slow queries: 0 Opens: 251 Flush tables: 3 Open tables: 191 Queries per second avg: 0.002"
// ORACLE_NULLS: NATURAL
// CLIENT_VERSION: "mysqlnd 5.0.12-dev - 20150407 - $Id: 7cc7cc96e675f6d72e5cf0f267f48e167c2abb23 $"
// SERVER_VERSION: "8.0.17"
// EMULATE_PREPARES: 0
// CONNECTION_STATUS: "127.0.0.1 via TCP/IP"
// DEFAULT_FETCH_MODE: ASSOC
// }
});
大家自己试一下输出的结果,会发现一个重大的问题,我们获得的数据还是 stdClass 的对象啊,没有变成数组。惊不惊喜,意不意外?而且我们直接输出连接生成的 PDO 会看到 DEFAULT_FETCH_MODE 确实是被设置成 ASSOC 了,这是为什么呢?不要着急,想想 PDO 在什么地方还能决定输出的结果,提示一下 PDOStatement 最后要执行什么。
没错,最后在 fetch() 的时候,其实还可以设置 FETCH_MODE ,而且这个地方设置的结果会影响最终返回的内容。那么我们就深入源码看一下是不是这样。找到 laravel/framework/src/Illuminate/Database/Connection.php 中的 select() 方法,也就是 原生语句 执行的地方。之前我们已经说过,查询构造器 最终调用的结果还是使用的 原生查询 的这几个方法,所以我们从这个 select() 方法入手。在这个方法中,会调用一个 prepared() 方法,来看看这个方法在干什么。
代码语言:javascript复制protected function prepared(PDOStatement $statement)
{
$statement->setFetchMode($this->fetchMode);
$this->event(new StatementPrepared(
$this, $statement
));
return $statement;
}
果然不出所料,它在这里调用了 PDO 的 setFetchMode() 方法设置 FETCH_MODE 。接着我们看一下这个 $this->fetchMode 是什么内容。
代码语言:javascript复制protected $fetchMode = PDO::FETCH_OBJ;
这是一个写死了的属性,写死了,死了,了。我去,这意思是没法修改它了?而且找遍整个数据库组件源码中,你都找不到可以重新设置这个属性的地方。难道我们就没办法修改 FETCH_MODE 了吗?仔细看上面的 prepared() 方法,在 setFetchMode() 之后又干了什么。
event() 是注册一个事件,传递进去的是一个 StatmentPrepared 对象,这个对象有两个构造参数,一个是连接对象本身,一个是我们生成的 PDOSatement 对象。这里是不是有什么玄机呢?
如果你去网上搜索如何让 Laravel 返回的结果变成数组的话,那么大部分都会给出下面这段代码。
代码语言:javascript复制// app/Providers/EventServiceProvider.php
public function boot()
{
//
Event::listen(StatementPrepared::class, function ($event) {
$event->statement->setFetchMode(PDO::FETCH_ASSOC);
});
}
在 app/Providers/EventServiceProvider.php 文件中的 boot() 方法里面,添加一个 StatementPrepared 对象的事件监听,在这个监听器的回调方法里面,就可以修改默认的 FETCH_MODE ,是不是和前面的 prepared() 代码中的事件注册对应上了。事件,就是要有一个注册,然后在另外一个地方监听,当注册的对象内容发生变化的时候,可以通过监听这边的方法来对事件内容进行处理。关于 Laravel 事件的内容,我们将在后面的文章中进行详细的学习。
现在,你再回到路由中去测试我们查询的结果,就会发现输出的内容是符合我们预期的数组格式了。这个时候又来了一个新的问题,貌似所有的连接都被修改成这种形式了,但是我之前的代码已经写成对象形式了,能不能单独针对某一个连接配置修改呢?当然可以,别忘了,我们的 StatementPrepared 有两个构造参数,第一个参数是连接对象呀。
代码语言:javascript复制public function boot()
{
//
Event::listen(StatementPrepared::class, function ($event) {
dump($event);
// #config: array:15 [
// "driver" => "mysql"
// "host" => "127.0.0.1"
// "port" => "3306"
// "database" => "laravel"
// "username" => "root"
// "password" => ""
// "unix_socket" => ""
// "charset" => "utf8mb4"
// "collation" => "utf8mb4_unicode_ci"
// "prefix" => ""
// "prefix_indexes" => true
// "strict" => true
// "engine" => null
// "options" => array:1 [▶]
// "name" => "mysql3"
// ]
if($event->connection->getConfig('name') == 'mysql3'){
$event->statement->setFetchMode(PDO::FETCH_ASSOC);
}
});
}
回调函数的参数,也就是这个 $event 就是 StatementPrepared 对象实例,从它这里我们就能得到事件注册时获得的 Connection 对象。在 Connection 对象的 config 属性中,清晰地记录着我们的 config/database.php 中的配置信息。然后,根据配置名称进行判断就好啦。相信剩下的事情就不用我多说了。
总结
没说错吧,今天的内容非常简单,但是虽说简单确又很实用。事务的作用不必多说,但它在框架中的实现其实是非常简单的,就是针对原始 PDO 的一个封装,大家很容易就可以找到源码。而修改 FETCH_MODE 是非常特殊的一个情况,其它的 PDO 属性基本都是可以在配置文件中直接指定的,唯独这个 FETCH_MODE 的设置是比较特殊的。当然,这也和框架的理念有关,毕竟我们是优美的框架,那必然也是面向对象的,所以就像 Java 中的 JavaBean 一样,Laravel 也是更推荐使用对象的方式来操作数据,而且更推荐的是使用 Model 。还记得吗,在 Model 中查询返回的结果,每条数据都会直接是这个 Model 对象,而不是 stdClass ,这一点,就真的和 JavaBean 是完全相同的概念了。
另外还需要注意的一点是,Model 查询的结果如果使用了 toArray() 的话,返回的数据直接就是数组格式的,为什么呢?卖个关子,大家在 laravel/framework/src/Illuminate/Database/Query/Builder.php 中找一下 toArray() 的源码实现,然后再去看一下所有 Model 的基类 laravel/framework/src/Illuminate/Database/Eloquent/Model.php 实现了哪个接口,相信大家马上就能明白了。
参考文档:
https://learnku.com/docs/laravel/8.x/queries/9401