美文网首页ormLaravel开发实践程序员
orm 系列 之 Eloquent演化历程1

orm 系列 之 Eloquent演化历程1

作者: 小聪明李良才 | 来源:发表于2016-11-25 14:35 被阅读662次
    Eloquent

    Eloquent是laravel中的orm,采取的是active record的设计模式,里面的对象不仅包括领域逻辑,还包括了数据库操作,但是大家平时使用的时候可能没有探究eloquent是怎么设计的,active record这种模式的优缺点等问题,下面我会带领大家从头开始看看Eloquent是如何设计并实现的。

    本文是orm系列的第二篇,也是Eloquent演化的第一篇,Eloquent系列会尝试着讲清楚Eloquent是如何一步一步演化到目前功能强大的版本的,但是毕竟个人能力有限,不可能分析的非常完善,总会有不懂的地方,所以讲的不错误的地方,恳请大牛们能指出,或者如果你有什么地方是没看懂的,也请指出问题来,因为可能那地方就是我自己没看懂,所以没讲明白,也请提出来,然后我们一起讨论的,让我们能共同的进步的。

    初始化

    Eloquent首先要对数据库连接做抽象,于是有了Connection类,内部主要是对PDO的一个封装,但是如果只有Connection的话,一个问题是,我们需要直面sql,于是就有了Builder类,其功能就是屏蔽sql,让我们能用面向对象的方式来完成sql的查询功能,Builder应该是sql builder,此时Eloquent的主要的类就如下:

    其中Builder负责sql的组装,Connection负责具体的数据库交互,其中多出来一个Grammar,其负责主要是负责将Builder里面存储的数据转化为sql。

    note:此处版本是54d73c6,通过 git co 54d73c6 可以查看

    model引入

    接着我们继续演化,要引进Model,要实现Active Record模式,在46966ec中首次加入了Eloquent/Model类,有兴趣的同学可以git co 46966ec查看,刚提交上来的时候,Model类中大概如下:

    可以看到属性通过定义table,connection,将具体的数据库操作是委托给了connection类,然后Model自己是负责领域逻辑,同时会定义一些静态方法,如create,find,save,充当了Row Data Gateway角色,此时的类图如下:

    此时新增的Model类直接依赖于Connection和Builder,带来的问题是耦合,于是就有了一个改动,在Model同一层级上引入了一新的Builder,具体通过git co c420bd8查看。

    use Illuminate\Database\Query\Builder as BaseBuilder;
    
    class Builder extends BaseBuilder {
        /**
         * The model being queried.
         *
         * @var Illuminate\Database\Eloquent\Model
         */
        protected $model;
        ....
    }
    

    里面具体就是在基础BaseBuilder上通过Model来获取一些信息设置,譬如$this->from($model->getTable())这种操作,还有一个好处是保持了BaseBuilder的纯净,没有形成Model和BaseBuilder之间的双向依赖,通过Model同层的Builder来去耦合,如下图所示:

    relation进入

    下一步是要引入1-1,1-N,N-N的关系了,可以通过git co 912de03查看,此时一个新增的类的情况如下:

    ├── Builder.php
    ├── Model.php
    └── Relations
        ├── BelongsTo.php
        ├── BelongsToMany.php
        ├── HasMany.php
        ├── HasOne.php
        ├── HasOneOrMany.php
        └── Relation.php
    

    其中Relation是基类,然后其他的几个都继承它。

    此时关系处理上主要的逻辑是调用Model的HasOne等表关系的方法,返回Relation的子类,然后通过Relation来处理进而返回数据,这么说可能有点绕,我们下面具体介绍下每个关系的实现,大家可能就理解了。

    先看HasOne,即OneToOne的关系,看代码

    public function hasOne($related, $foreignKey = null)
    {
      $foreignKey = $foreignKey ?: $this->getForeignKey();
    
      $instance = new $related;
    
      return new HasOne($instance->newQuery(), $this, $foreignKey);
    }
    

    我们看到当调用Model的hasOne方法后,返回是一个HasOne,即Relation,当我们调用Relation的方法时,是怎么处理的呢?通过魔术方法__call,将其委托给了Eloquent\Builder

    public function __call($method, $parameters)
    {
      if (method_exists($this->query, $method))
      {
        return call_user_func_array(array($this->query, $method), $parameters);
      }
    
      throw new \BadMethodCallException("Method [$method] does not exist.");
    }
    

    即其实Relation是对Eloquent\Builder的一个封装,支持面向对象式的sql操作,我们下面来看下当我们使用HasOne的时候发生了什么。

    假设我们有个User,Phone,然后User和Phone的关系是HasOne,在User声明上就会有

    class User extends Model
    {
        /**
         * Get the phone record associated with the user.
         */
        public function phone()
        {
            return $this->hasOne('App\Phone');
        }
    }
    

    此时HasOne的构造函数如下:

    // builder是Eloquent\Builder, parent是Uer,$foreign_key是user_id
    $relation = new HasOne($builder, $parent, $foreign_key);
    

    当使用User::with('phone')->get()的时候,就会去eager load进phone了,具体的过程中,在调用Eloquent\Builder的get的时候,里面有个逻辑是:

    if (count($models) > 0)
    {
      $models = $this->eagerLoadRelations($models);
    }
    

    获取has one关系,我们跟着看到代码,会调用到函数eagerLoadRelation,具体看代码:

    protected function eagerLoadRelation(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);
    }
    

    其中getRelation会调用到User()->phone(),即此处$instanceHasOne,接着调用HasOne->addEagerConstraints()HasOne->initializeRelation(),具体的代码是:

    // class HasOne
    public function addEagerConstraints(array $models)
    {
      // 新增foreignKey的条件
      $this->query->whereIn($this->foreignKey, $this->getKeys($models));
    }
    public function initRelation(array $models, $relation)
    {
      foreach ($models as $model)
      {
        $model->setRelation($relation, null);
      }
    
      return $models;
    }
    // class Model
    public function setRelation($relation, $value)
    {
      $this->relations[$relation] = $value;
    }
    

    最后调用match方法,就是正确的给每个model设置好relation关系。

    以上就是我们分析的HasOne的实现,其他的关系都类似,此处不再重复,然后eager load的含义是指,当我们要加载多个数据的时候,我们尽可能用一条sql解决,而不是多条sql,具体来说如果我们有多个Users,需要加载Phones的,如果不采用eager,在每个sql就是where user_id=?,而eager模式则是where user_id in (?,?,?),这样差异就很明显了.

    note:以上分析的代码是:git co f6e2170

    讲到这,我们列举下对象之间的关系

    One-To-One

    User 和 Phone的1对1的关系,

    class User extends Model
    {
        /**
         * Get the phone record associated with the user.
         */
        public function phone()
        {
            return $this->hasOne('App\Phone');
        }
    }
    // 逆向定义
    class Phone extends Model
    {
        /**
         * Get the user that owns the phone.
         */
        public function user()
        {
            return $this->belongsTo('App\User');
        }
    }
    

    sql的查询类似于下面

    select id from phone where user_id in (1)
    
    select id from user where id in (phone.user_id)
    

    One-To-Many

    以Post和Comment为例,一个Post会有多个Comment

    
    class Post extends Model
    {
        /**
         * Get the comments for the blog post.
         */
        public function comments()
        {
            return $this->hasMany('App\Comment');
        }
    }
    // reverse
    class Comment extends Model
    {
        /**
         * Get the post that owns the comment.
         */
        public function post()
        {
            return $this->belongsTo('App\Post');
        }
    }
    

    此处的sql和HasOne一致

    select id from comment where post_id in (1)
    
    select id from post where id in (comment.post_id)
    

    Many To Many

    以user和role为例,一个用户会有不同的角色,一个角色也会有不同的人,这个时候就需要一张中间表role_user,代码声明上如下:

    class User extends Model
    {
        /**
         * The roles that belong to the user.
         */
        public function roles()
        {
            return $this->belongsToMany('App\Role');
        }
    }
    class Role extends Model
    {
        /**
         * The users that belong to the role.
         */
        public function users()
        {
            return $this->belongsToMany('App\User');
        }
    }
    

    这个关系我们稍微具体讲下,我们在使用上可能会是下面这样子的

    return $this->belongsToMany('App\Role', 'user_roles', 'user_id', 'role_id');
    

    在构造函数中,会调用addConstraints方法,如下

    // class belongsToMany
    public function addConstraints()
    {
        $this->setSelect()->setJoin()->setWhere();
    }
    

    此处会预先设置setSelect()->setJoin()->setWhere(),作用分别是:

    setSelect() : 在select的字段中新增 role.*,user_role.id as pivot_id

    setJoin():新增join, join user_role on role.id = user_role.role_id,联合查询

    setWhere():新增 user_id = ?

    查询的表是role,join表user_role

    在get的时候,其逻辑和HasOne等关系也所有不同,代码如下:

    // class belongsToMany
    public function get($columns = array('*'))
    {
      $models = $this->query->getModels($columns);
    
      $this->hydratePivotRelation($models);
    
      if (count($models) > 0)
      {
        $models = $this->query->eagerLoadRelations($models);
      }
    
      return new Collection($models);
    }
    

    此处有个方法叫hydratePivotRelation,我们进入看下到底是怎么回事

    // class belongsToMany
    protected function hydratePivotRelation(array $models)
    {
     // 将中间记录取出来,设置属性pivot为Model pivot
      foreach ($models as $model)
      {
        $values = $this->cleanPivotAttributes($model);
    
        $pivot = $this->newExistingPivot($values);
    
        $model->setRelation('pivot', $pivot);
      }
    }
    

    其实做的事情就是设置了Role的pivot属性。

    到这,我们就分析完了eloquent在f6e2170版本上具有的功能了,到目前为止,eloquent的类图如下:

    总结

    目前,我们分析到的版本是f6e2170,已经具备了一个orm该需要的功能了,Connection负责数据库操作,Builder负责面向对象的sql操作,Grammar负责sql的拼装,Eloquent/Model是Active Record模式的核心Model,同时具备领域逻辑和数据库操作功能,其中数据库操作功能是委托给了Eloquent/Builder,同时我们也定义了对象的3种关系,1-1,1-N,N-N,下一阶段,Eloquent将会实现migrations or database modification logic的功能,尽情期待。

    这是orm的第二篇,你的鼓励是我继续写下去的动力,期待我们共同进步。

    这个时代,每个人都是超级个体!关注我,一起成长!

    相关文章

      网友评论

      本文标题:orm 系列 之 Eloquent演化历程1

      本文链接:https://www.haomeiwen.com/subject/jhtwpttx.html