【Laravel系列4.6】

2023-03-03 13:16:59 浏览数 (3)

事务以及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

0 人点赞