orm 系列 之 Eloquent演化历程1
hjim2904
8年前
<p><img src="https://simg.open-open.com/show/5515b08d1d2f99e44446d1f35425aa5d.png"></p> <p>Eloquent是laravel中的orm,采取的是active record的设计模式,里面的对象不仅包括领域逻辑,还包括了数据库操作,但是大家平时使用的时候可能没有探究eloquent是怎么设计的,active record这种模式的优缺点等问题,下面我会带领大家从头开始看看Eloquent是如何设计并实现的。</p> <p>本文是orm系列的第二篇,也是Eloquent演化的第一篇,Eloquent系列会尝试着讲清楚Eloquent是如何一步一步演化到目前功能强大的版本的,但是毕竟个人能力有限,不可能分析的非常完善,总会有不懂的地方,所以讲的不错误的地方, <strong>恳请大牛们能指出</strong> ,或者如果你有什么地方是没看懂的,也请 <strong>指出问题</strong> 来,因为可能那地方就是我自己没看懂,所以没讲明白,也请提出来,然后我们 <strong>一起讨论</strong> 的,让我们能 <strong>共同的进步</strong> 的。</p> <h2>初始化</h2> <p>首先要对数据库连接做抽象,于是有了 Connection 类,内部主要是对 PDO 的一个封装,但是如果只有Connection的话,一个问题是,我们需要直面sql,于是就有了 Builder 类,其功能就是屏蔽sql,让我们能用面向对象的方式来完成sql的查询功能, Builder 应该是sql builder,此时Eloquent的主要的类就如下:</p> <p><img src="https://simg.open-open.com/show/3f48fee7425a58b17add95a62d4b5708.png"></p> <p>其中Builder负责sql的组装,Connection负责具体的数据库交互,其中多出来一个Grammar,其负责主要是负责将Builder里面存储的数据转化为sql。</p> <p>note:此处版本是54d73c6,通过 git co 54d73c6 可以查看</p> <h2>model引入</h2> <p>接着我们继续演化,要引进Model,要实现Active Record模式,在 <strong>46966ec</strong> 中首次加入了 Eloquent/Model 类,有兴趣的同学可以 git co 46966ec 查看,刚提交上来的时候,Model类中大概如下:</p> <p><img src="https://simg.open-open.com/show/fbe966d7dac3e7c3649308ea95592122.png"></p> <p>可以看到属性通过定义table,connection,将具体的数据库操作是委托给了 connection 类,然后Model自己是负责领域逻辑,同时会定义一些静态方法,如 create,find,save ,充当了 <em>Row Data Gateway</em> 角色,此时的类图如下:</p> <p><img src="https://simg.open-open.com/show/64016643884af870bcd8e646df8e1ef9.png"></p> <p>此时新增的Model类直接依赖于Connection和Builder,带来的问题是耦合,于是就有了一个改动,在Model同一层级上引入了一新的Builder,具体通过 git co c420bd8 查看。</p> <pre> <code class="language-java">useIlluminate\Database\Query\BuilderasBaseBuilder; classBuilderextendsBaseBuilder{ /** * The model being queried. * * @varIlluminate\Database\Eloquent\Model */ protected$model; .... } </code></pre> <p>里面具体就是在基础 BaseBuilder 上通过 Model 来获取一些信息设置,譬如 $this->from($model->getTable()) 这种操作,还有一个好处是保持了 BaseBuilder 的纯净,没有形成Model和BaseBuilder之间的双向依赖,通过Model同层的Builder来去耦合,如下图所示:</p> <p><img src="https://simg.open-open.com/show/fe3eccfd6b96337e2854516afc3e663e.png"></p> <h2>relation进入</h2> <p>下一步是要引入1-1,1-N,N-N的关系了,可以通过 git co 912de03 查看,此时一个新增的类的情况如下:</p> <pre> <code class="language-java">├── Builder.php ├── Model.php └── Relations ├── BelongsTo.php ├── BelongsToMany.php ├── HasMany.php ├── HasOne.php ├── HasOneOrMany.php └── Relation.php </code></pre> <p>其中 Relation 是基类,然后其他的几个都继承它。</p> <p>此时关系处理上主要的逻辑是调用Model的HasOne等表关系的方法,返回Relation的子类,然后通过Relation来处理进而返回数据,这么说可能有点绕,我们下面具体介绍下每个关系的实现,大家可能就理解了。</p> <p>先看HasOne,即OneToOne的关系,看代码</p> <pre> <code class="language-java">publicfunctionhasOne($related, $foreignKey = null) { $foreignKey = $foreignKey ?: $this->getForeignKey(); $instance = new$related; returnnewHasOne($instance->newQuery(),$this, $foreignKey); } </code></pre> <p>我们看到当调用Model的 hasOne 方法后,返回是一个HasOne,即Relation,当我们调用Relation的方法时,是怎么处理的呢?通过魔术方法 __call ,将其委托给了 Eloquent\Builder ,</p> <pre> <code class="language-java">publicfunction__call($method, $parameters) { if(method_exists($this->query, $method)) { returncall_user_func_array(array($this->query, $method), $parameters); } thrownew\BadMethodCallException("Method [$method] does not exist."); } </code></pre> <p>即其实Relation是对 Eloquent\Builder 的一个封装,支持面向对象式的sql操作,我们下面来看下当我们使用HasOne的时候发生了什么。</p> <p>假设我们有个User,Phone,然后User和Phone的关系是HasOne,在User声明上就会有</p> <pre> <code class="language-java">classUserextendsModel { /** * Get the phone record associated with the user. */ publicfunctionphone() { return$this->hasOne('App\Phone'); } } </code></pre> <p>此时HasOne的构造函数如下:</p> <pre> <code class="language-java">// builder是Eloquent\Builder, parent是Uer,$foreign_key是user_id $relation = newHasOne($builder, $parent, $foreign_key); </code></pre> <p>当使用 User::with('phone')->get() 的时候,就会去eager load进phone了,具体的过程中,在调用 Eloquent\Builder 的get的时候,里面有个逻辑是:</p> <pre> <code class="language-java">if(count($models) >0) { $models = $this->eagerLoadRelations($models); } </code></pre> <p>获取has one关系,我们跟着看到代码,会调用到函数 eagerLoadRelation ,具体看代码:</p> <pre> <code class="language-java">protectedfunctioneagerLoadRelation(array $models, $relation, Closure $constraints) { $instance = $this->getRelation($relation); ... $instance->addEagerConstraints($models); $models = $instance->initializeRelation($models, $relation); $results = $instance->get(); return$instance->match($models, $results, $relation); } </code></pre> <p>其中 getRelation 会调用到 User()->phone() ,即此处 $instance 是 HasOne ,接着调用 HasOne->addEagerConstraints() 和 HasOne->initializeRelation() ,具体的代码是:</p> <pre> <code class="language-java">// class HasOne publicfunctionaddEagerConstraints(array $models) { // 新增foreignKey的条件 $this->query->whereIn($this->foreignKey,$this->getKeys($models)); } publicfunctioninitRelation(array $models, $relation) { foreach($modelsas$model) { $model->setRelation($relation, null); } return$models; } // class Model publicfunctionsetRelation($relation, $value) { $this->relations[$relation] = $value; } </code></pre> <p>最后调用match方法,就是正确的给每个model设置好relation关系。</p> <p>以上就是我们分析的HasOne的实现,其他的关系都类似,此处不再重复,然后eager load的含义是指,当我们要加载多个数据的时候,我们尽可能用一条sql解决,而不是多条sql,具体来说如果我们有多个Users,需要加载Phones的,如果不采用eager,在每个sql就是 where user_id=? ,而eager模式则是 where user_id in (?,?,?) ,这样差异就很明显了.</p> <p>note:以上分析的代码是:git co f6e2170</p> <p>讲到这,我们列举下对象之间的关系</p> <h3>One-To-One</h3> <p>User 和 Phone的1对1的关系,</p> <pre> <code class="language-java">classUserextendsModel { /** * Get the phone record associated with the user. */ publicfunctionphone() { return$this->hasOne('App\Phone'); } } // 逆向定义 classPhoneextendsModel { /** * Get the user that owns the phone. */ publicfunctionuser() { return$this->belongsTo('App\User'); } } </code></pre> <p>sql的查询类似于下面</p> <pre> <code class="language-java">select id from phone where user_id in (1) select id from user where id in (phone.user_id) </code></pre> <h3>One-To-Many</h3> <p>以Post和Comment为例,一个Post会有多个Comment</p> <pre> <code class="language-java">classPostextendsModel { /** * Get the comments for the blog post. */ publicfunctioncomments() { return$this->hasMany('App\Comment'); } } // reverse classCommentextendsModel { /** * Get the post that owns the comment. */ publicfunctionpost() { return$this->belongsTo('App\Post'); } } </code></pre> <p>此处的sql和HasOne一致</p> <pre> <code class="language-java">select id from comment where post_id in (1) select id from post where id in (comment.post_id) </code></pre> <h3>Many To Many</h3> <p>以user和role为例,一个用户会有不同的角色,一个角色也会有不同的人,这个时候就需要一张中间表 <em>role_user</em> ,代码声明上如下:</p> <pre> <code class="language-java">classUserextendsModel { /** * The roles that belong to the user. */ publicfunctionroles() { return$this->belongsToMany('App\Role'); } } classRoleextendsModel { /** * The users that belong to the role. */ publicfunctionusers() { return$this->belongsToMany('App\User'); } } </code></pre> <p>这个关系我们稍微具体讲下,我们在使用上可能会是下面这样子的</p> <pre> <code class="language-java">return$this->belongsToMany('App\Role','user_roles','user_id','role_id'); </code></pre> <p>在构造函数中,会调用 addConstraints 方法,如下</p> <pre> <code class="language-java">// class belongsToMany publicfunctionaddConstraints() { $this->setSelect()->setJoin()->setWhere(); } </code></pre> <p>此处会预先设置 setSelect()->setJoin()->setWhere() ,作用分别是:</p> <p>setSelect() : 在select的字段中新增 role.*,user_role.id as pivot_id</p> <p>setJoin():新增join, join user_role on role.id = user_role.role_id,联合查询</p> <p>setWhere():新增 user_id = ?</p> <p>查询的表是role,join表user_role</p> <p>在get的时候,其逻辑和HasOne等关系也所有不同,代码如下:</p> <pre> <code class="language-java">// class belongsToMany publicfunctionget($columns = array('*')) { $models = $this->query->getModels($columns); $this->hydratePivotRelation($models); if(count($models) >0) { $models = $this->query->eagerLoadRelations($models); } returnnewCollection($models); } </code></pre> <p>此处有个方法叫 hydratePivotRelation ,我们进入看下到底是怎么回事</p> <pre> <code class="language-java">// class belongsToMany protectedfunctionhydratePivotRelation(array $models) { // 将中间记录取出来,设置属性pivot为Model pivot foreach($modelsas$model) { $values = $this->cleanPivotAttributes($model); $pivot = $this->newExistingPivot($values); $model->setRelation('pivot', $pivot); } } </code></pre> <p>其实做的事情就是设置了Role的pivot属性。</p> <p>到这,我们就分析完了eloquent在 <strong>f6e2170</strong> 版本上具有的功能了,到目前为止,eloquent的类图如下:</p> <p><img src="https://simg.open-open.com/show/134d6a16d9a6ef3d183cd101eb36e761.png"></p> <h2>总结</h2> <p>目前,我们分析到的版本是 <strong>f6e2170</strong> ,已经具备了一个orm该需要的功能了, Connection 负责数据库操作, Builder 负责面向对象的sql操作, Grammar 负责sql的拼装, Eloquent/Model 是Active Record模式的核心Model,同时具备领域逻辑和数据库操作功能,其中数据库操作功能是委托给了 Eloquent/Builder ,同时我们也定义了对象的3种关系,1-1,1-N,N-N,下一阶段,Eloquent将会实现 <strong>migrations or database modification logic</strong> 的功能,尽情期待。</p> <p> </p> <p>来自:http://blog.zhuanxu.org/2016-11-24-eloquent-1.html</p> <p> </p>