orm 系列 之 Eloquent演化历程1

hjim2904 5年前
   <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>