标题:【Laravel系列4.1】连接数据库与原生查询

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

    连接数据库与原生查询

    在 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

    视频链接

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

    微信视频地址:http://mp.weixin.qq.com/s?__biz=MzIxODQyNTU1MA==&amp;mid=2247486940&amp;idx=1&amp;sn=20cef39276e45970585134d0863bc138&amp;chksm=97ebfc7da09c756b86950c034ffa6e7b02036c31cf0e70c6852f5ef1d001cfbc3032ef8c5e15&amp;scene=27#wechat_redirect

    微信文章地址:http://mp.weixin.qq.com/s?__biz=MzIxODQyNTU1MA==&amp;mid=2247486524&amp;idx=1&amp;sn=23d216cfc274f541e887273cba5cca0e&amp;chksm=97ebfd9da09c748b1f7fc3e123a6c1f8e4de6c9ca656c54fff6a925ca30b64e88dc9e156b055&amp;scene=27#wechat_redirect

    搜索
    关注