标题:【Laravel系列4.6】事务以及PDO属性设置

文章目录
    分类:PHP 标签:Laravel,PHP框架

    事务以及PDO属性设置

    今天学习的内容比较轻松,就讲两个小东西,而且也没什么特别的源码方面的内容。主要也是因为这两个小功能的应用会比较广泛,并且源码实现也非常简单易懂,我就简单的说一下源码大概的位置,大家直接自己看一下就好了。因此,这篇文章也可以看成是本系列教程学习的一个中场休息。

    事务

    对于数据库来说,事务操作是非常经典而且也很实用的一个技术。具体事务是干什么的我们就不多说了,毕竟这也不是数据库知识普及的文章。在电商、金融类应用中,事务是非常重要的功能,也是必须的能力。在 Laravel 中操作事务可以说是简单到没朋友。

    Route::get('db/tran/insert', function(){
        \Illuminate\Support\Facades\DB::beginTransaction();
        try {
            \Illuminate\Support\Facades\DB::table('db_test')->insert(['name' => 'Lily', 'sex' => 2]);
            \Illuminate\Support\Facades\DB::table('db_test_no')->insert(['name' => 'Lily', 'sex' => 2]);
            \Illuminate\Support\Facades\DB::commit();
        }catch(Exception $e){
            \Illuminate\Support\Facades\DB::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 的默认属性。

    '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 。然后我们建一个路由来测试一下。

    Route::get('db/collection/list', function(){
        dump( \Illuminate\Support\Facades\DB::connection('mysql3')->table('db_test')->get()->toArray());
        dump(\Illuminate\Support\Facades\DB::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() 方法,来看看这个方法在干什么。

    protected function prepared(PDOStatement $statement)
    {
        $statement->setFetchMode($this->fetchMode);
    
        $this->event(new StatementPrepared(
            $this, $statement
        ));
    
        return $statement;
    }

    果然不出所料,它在这里调用了 PDO 的 setFetchMode() 方法设置 FETCH_MODE 。接着我们看一下这个 $this->fetchMode 是什么内容。

    protected $fetchMode = PDO::FETCH_OBJ;

    这是一个写死了的属性,写死了,死了,了。我去,这意思是没法修改它了?而且找遍整个数据库组件源码中,你都找不到可以重新设置这个属性的地方。难道我们就没办法修改 FETCH_MODE 了吗?仔细看上面的 prepared() 方法,在 setFetchMode() 之后又干了什么。


    event() 是注册一个事件,传递进去的是一个 StatmentPrepared 对象,这个对象有两个构造参数,一个是连接对象本身,一个是我们生成的 PDOSatement 对象。这里是不是有什么玄机呢?


    如果你去网上搜索如何让 Laravel 返回的结果变成数组的话,那么大部分都会给出下面这段代码。

    // 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 有两个构造参数,第一个参数是连接对象呀。

    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

    视频链接

    微信视频地址:http://mp.weixin.qq.com/s?__biz=MzIxODQyNTU1MA==&mid=2247487030&idx=1&sn=41697b54fb5de4aeab23e4d043680c25&chksm=97ebff97a09c7681992bb1f7b3e62434446dbc86d9de8b647aabd3046e63cc145fdf6c5c6849&scene=27#wechat_redirect

    微信文章地址:http://mp.weixin.qq.com/s?__biz=MzIxODQyNTU1MA==&mid=2247486682&idx=1&sn=942425aa8a3652da0c030b07b95bf037&chksm=97ebfd7ba09c746d8558620a64d44246f601710e7235d9f68d9944a212f1110818e891bdf6c3&scene=27#wechat_redirect

    B站视频地址:https://www.bilibili.com/video/BV1jt4y1h7tH

    搜索
    关注