Laravel 模型、Builder 及数据库操作的解读

估计使用 Laravel 的小伙伴对 Model, Builder, Query 等操作都已经非常熟悉了,用起来也确实简单,不需要去处理数据库连接,不用写原生的 sql 语句(正是因为这样,所以我现在对 sql 语句其实很生疏...)。但是我在使用的时候,对 Modle, Eloquent Builder, Query Builder 这几个总会搞的一团糟,比如 User::find(1),明明 Model 里面并没有这个方法,那到底调用的是哪里的方法呢?随着用的越多,类似的疑惑也越多,所以花了半天的时间阅读了 Illuminate\Database 里面的代码,于是有了这篇文章。可能有些理解不对的地方,希望多多指教。

从 ServiceProvider 开始

阅读 Laravel 的源码,我建议从 ServiceProvider 开始,因为 ServiceProvider 里面涉及到的是一些比较重要的初始化操作,包括服务容器绑定和初始化设置等等,比如使用 DB facade,如果不看绑定的话,可能就不知道实际上这个 facade 对应的类是 Illuminate\Database\DatabaseManager

先看看 Illuminate\Database\DatabaseServiceProvider,在这个 ServiceProvider 里面绑定了两个单例:
db.factory => ConnectionFactorydb =>DatabaseManager 。从名称上来看就可以理解:ConnectionFactory 是创建数据库 connection 的工厂,而 DatabaseManager 则是管理不同数据库 connection 的管理者。

另外一个很重要的是在 boot 方法里面: Model::setConnectionResolver($this->app['db']),给模型设置 connection resolver,因为我们知道 DatabaseManager 就是管理数据库 connection 的,给模型设置了 DatabaseManager 的话,模型就可以获取到相应的数据库 connection。

到这里初始化工作完成(当然还有其他的绑定和启动等等,这里不关注)。

简单的查询流程

在系统初始化之后,Model 已经准备好了,现在从一条简单的 User::find(1)查询开始,看看这条查询如何处理的。

  1. 首先调用 User 模型的静态 find 方法,在 User 和 基类 Model 中都没有 find 静态方法,于是调用基类 Model 的 __callStatic 魔术方法;

  2. __callStatic 魔术方法创建了一个新的 User 实例 $instance = new static;,并调用 $instance 的非静态 find 方法 call_user_func_array([$instance, $method], $parameters)

  3. User 和 基类 Model 中也没有非静态 find 方法,于是调用基类 Model 的 __call 魔术方法;

  4. __call 魔术创建了一个 \Illuminate\Database\Eloquent\Builder 实例 $query = $this->newQuery();,并调用 Eloquent Builder 的 find 方法 call_user_func_array([$query, $method], $parameters);

  5. Eloquent Builder 实例需要一个 Query Builder 来创建,于是先要创建一个 Query Builder 实例;

  6. Query Builder 实例又需要一个 connection,grammar,processor 来创建,于是调用 Modle 中的 DatabaseManager 获取一个对应 $connection 名称的 connection,并获取相应的 grammar 和 processor。所以我们可以通过设置模型的 $connection 属性来设置指定的数据库链接,默认则为 config.database.default 的值;

  7. 在 Eloquent Builder 的 find 方法中,通过调用 Query Builder 的 where 方法,构造一个含查询条件的 Query Builder,最后调用 Eloquent Builder 的 $this->take(1)->get($columns)->first() 方法,实际上也是通过 __call 魔术方法调用 Query Builder 的相应方法获取数据,实际上调用的是 $queryBuilder->tack(1)->get($columns)->first();

  8. Query Builder暴露出来获取结果的正是 get() 方法!看看对应的返回代码:return $this->processor->processSelect($this, $this->runSelect());,processor 是创建 Query Builder时获取的 connection 与之对应的 processor,用来处理返回结果;

  9. 接下来就是调用 Query Builder 的 runSelect 方法获取查询结果了 return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo);,toSql()方法通过 grammar 将 Query Builder 的查询条件转换成一条 sql 语句return $this->grammar->compileSelect($this);,而 connection 的 select 方法才真正运行 sql 语句;

整个架构

整个 Database 部分分为两层,一个是 Model + Eloquent Builder 的上层接口层,一部分是 Query Builder + (Collector + Collection + Grammar + Processor) 处理数据库的底层。

Query

其中 Collector + Collection + Grammar + Processor 对应不同的数据库有不同的实现,Laravel 默认支持的数据库有 Mysql、Postgres、SQLite、SqlServer,每一种数据都有一套对应的 Collector + Collection + Grammar + Processor。

Collector 配置了不同数据库的连接方式,主要实现了 connect() 方法,通过 ConnectionFactory 传递的 的配置信息连接到数据库,返回 pdo。

Grammar 将包含查询条件的 Query Builder 解析成 sql 语句。

Collection 保存着 pdo,处理基础的数据库操作,比如查询、插入、更新、删除、获取数据库名、表名等等基础的操作。

Processor 定义了对返回结果的处理。

Query Builder 存储了一系列的查询条件,比如 where 方法,实际上是通过不同的转换后存储到自身的 wheres 数组中 $this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');,而 select 方法则是存储到 columns 数组中,数据则存储在 bindings 属性中。那到底何时才查询的呢?估计很多新手有过那种情况,写了一堆查询条件,最后忘记了 get()了,返回的就是一个 Query Builder 的实例,所以,真正的查询就是在上面介绍过的 get() 方法。get()方法先通过 Grammar 将自身的查询条件转换成 sql 查询语句,再通过 Collection 来获取结果,随后通过 Processor 来处理结果。

ConnectionFactory

ConnectionFactory 根据需要创建的 collection 名获取 config.database.conllection 的配置信息,根据这些配置创建相应的 Collector 并调用 connect 方法连接数据库。通过返回的 pdo 创建相应的 collection。

在设置中可以设置单独的只读数据库,就是在 ConnectionFactory 里面判断的,如果设置了 read 类型的数据库,会调用 conllection 的 setReadPdo 方法,设置一个单独的 readPdo。

DatabaseManager

我们在设置中定义了 default 的 conllection,在模型中也可以设置单独的 conllection,而不同的 collection 就是通过 DatabaseManager 管理,同样,app('db')、$app['db']、DB 这些获取的都是 DatabaseManager。

可以直接调用 connection($name) 方法创建一个新的连接。由于 DatabaseManager 在系统中注册成单例了,所以如果一个连接没有 disconnect($name) 的话是一直存在的,并不会销毁。如果要重新连接,也可以调用 reconnect($name) 方法。

Eloquent

Model + Eloquent Builder 则是一个 ORM 的实现,把 Model 和 Builder 分离,Model 更关注的是作为一个实体的自身属性,而 Builder 则很好的连接 Model 和 底层部分。

一些小知识

在阅读代码的时候,发现一些小知识,忽然有那种“原来可以那样”的感觉,写出来大家看看。

  1. 重写模型的 CREATED_AT UPDATED_AT 可以设置自定义的创建修改时间字段,当然创建 migration 的时候也记得要改。

  2. 可以在模型中添加 bootTraitName 方法来初始化一些可能 trait 需要用到初始化环境,当模型第一次启动的时候就会调用这个方法。

  3. increment decrement 方法可以给某个字段增加或减少一个数值,参数 1 是字段名,参数 2 是数量,默认为1;

  4. Query Builder 是我们使用数据库最底层的操作了,可能有人会说 DB::table 不就是调用的默认的 collection 的方法吗?对的,但是 DB::table 返回的就是一个根据默认 collection 创建的 Query Builder.当然,你确实可以使用 sql 语句查询: DB::select('select * fromusers',[]),还有很多方法,查看 Illuminate\Database\Connection

  5. Query Builder 里面的方法太可爱了,你可以这样用:User::select('*')->from('users')->where('id',1)->get(),是不是很像 sql 语句? 不过不要忘了 get();

就写那些吧,还有很多细节方面以及 Schema migration 等等没有写,大家可以按照这种思路自己试着去读代码,有的时候会不得不惊讶作者的一些处理方式,肯定也会给你带来一些新的思考方向。

第一次写技术文章,不得不佩服那些能把技术文章写的通俗易懂的大神,有的时候理解和自己表达出来差异很大,写的有些混乱,见谅,也欢迎指出错误之处。

本帖已被设为精华帖!
本帖由系统于 6年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!