orm 系列 之 Eloquent演化歷程1

Eloquent

Eloquent是laravel中的orm迄损,采取的是active record的設(shè)計(jì)模式,里面的對(duì)象不僅包括領(lǐng)域邏輯,還包括了數(shù)據(jù)庫(kù)操作,但是大家平時(shí)使用的時(shí)候可能沒(méi)有探究eloquent是怎么設(shè)計(jì)的汉矿,active record這種模式的優(yōu)缺點(diǎn)等問(wèn)題,下面我會(huì)帶領(lǐng)大家從頭開(kāi)始看看Eloquent是如何設(shè)計(jì)并實(shí)現(xiàn)的副编。

本文是orm系列的第二篇负甸,也是Eloquent演化的第一篇流强,Eloquent系列會(huì)嘗試著講清楚Eloquent是如何一步一步演化到目前功能強(qiáng)大的版本的痹届,但是畢竟個(gè)人能力有限呻待,不可能分析的非常完善,總會(huì)有不懂的地方队腐,所以講的不錯(cuò)誤的地方蚕捉,懇請(qǐng)大牛們能指出,或者如果你有什么地方是沒(méi)看懂的柴淘,也請(qǐng)指出問(wèn)題來(lái)迫淹,因?yàn)榭赡苣堑胤骄褪俏易约簺](méi)看懂,所以沒(méi)講明白为严,也請(qǐng)?zhí)岢鰜?lái)敛熬,然后我們一起討論的,讓我們能共同的進(jìn)步的第股。

初始化

Eloquent首先要對(duì)數(shù)據(jù)庫(kù)連接做抽象应民,于是有了Connection類(lèi),內(nèi)部主要是對(duì)PDO的一個(gè)封裝夕吻,但是如果只有Connection的話诲锹,一個(gè)問(wèn)題是,我們需要直面sql涉馅,于是就有了Builder類(lèi)归园,其功能就是屏蔽sql,讓我們能用面向?qū)ο蟮姆绞絹?lái)完成sql的查詢功能稚矿,Builder應(yīng)該是sql builder庸诱,此時(shí)Eloquent的主要的類(lèi)就如下:

其中Builder負(fù)責(zé)sql的組裝,Connection負(fù)責(zé)具體的數(shù)據(jù)庫(kù)交互晤揣,其中多出來(lái)一個(gè)Grammar偶翅,其負(fù)責(zé)主要是負(fù)責(zé)將Builder里面存儲(chǔ)的數(shù)據(jù)轉(zhuǎn)化為sql。

note:此處版本是54d73c6碉渡,通過(guò) git co 54d73c6 可以查看

model引入

接著我們繼續(xù)演化聚谁,要引進(jìn)Model,要實(shí)現(xiàn)Active Record模式滞诺,在46966ec中首次加入了Eloquent/Model類(lèi)形导,有興趣的同學(xué)可以git co 46966ec查看,剛提交上來(lái)的時(shí)候习霹,Model類(lèi)中大概如下:

可以看到屬性通過(guò)定義table,connection朵耕,將具體的數(shù)據(jù)庫(kù)操作是委托給了connection類(lèi),然后Model自己是負(fù)責(zé)領(lǐng)域邏輯淋叶,同時(shí)會(huì)定義一些靜態(tài)方法阎曹,如create,find,save,充當(dāng)了Row Data Gateway角色,此時(shí)的類(lèi)圖如下:

此時(shí)新增的Model類(lèi)直接依賴于Connection和Builder处嫌,帶來(lái)的問(wèn)題是耦合栅贴,于是就有了一個(gè)改動(dòng),在Model同一層級(jí)上引入了一新的Builder熏迹,具體通過(guò)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;
    ....
}

里面具體就是在基礎(chǔ)BaseBuilder上通過(guò)Model來(lái)獲取一些信息設(shè)置,譬如$this->from($model->getTable())這種操作注暗,還有一個(gè)好處是保持了BaseBuilder的純凈坛缕,沒(méi)有形成Model和BaseBuilder之間的雙向依賴,通過(guò)Model同層的Builder來(lái)去耦合捆昏,如下圖所示:

relation進(jìn)入

下一步是要引入1-1赚楚,1-N,N-N的關(guān)系了骗卜,可以通過(guò)git co 912de03查看宠页,此時(shí)一個(gè)新增的類(lèi)的情況如下:

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

其中Relation是基類(lèi),然后其他的幾個(gè)都繼承它膨俐。

此時(shí)關(guān)系處理上主要的邏輯是調(diào)用Model的HasOne等表關(guān)系的方法勇皇,返回Relation的子類(lèi),然后通過(guò)Relation來(lái)處理進(jìn)而返回?cái)?shù)據(jù)焚刺,這么說(shuō)可能有點(diǎn)繞敛摘,我們下面具體介紹下每個(gè)關(guān)系的實(shí)現(xiàn),大家可能就理解了乳愉。

先看HasOne兄淫,即OneToOne的關(guān)系,看代碼

public function hasOne($related, $foreignKey = null)
{
  $foreignKey = $foreignKey ?: $this->getForeignKey();

  $instance = new $related;

  return new HasOne($instance->newQuery(), $this, $foreignKey);
}

我們看到當(dāng)調(diào)用Model的hasOne方法后蔓姚,返回是一個(gè)HasOne捕虽,即Relation,當(dāng)我們調(diào)用Relation的方法時(shí)坡脐,是怎么處理的呢泄私?通過(guò)魔術(shù)方法__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.");
}

即其實(shí)Relation是對(duì)Eloquent\Builder的一個(gè)封裝晌端,支持面向?qū)ο笫降膕ql操作,我們下面來(lái)看下當(dāng)我們使用HasOne的時(shí)候發(fā)生了什么恬砂。

假設(shè)我們有個(gè)User,Phone咧纠,然后User和Phone的關(guān)系是HasOne,在User聲明上就會(huì)有

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

此時(shí)HasOne的構(gòu)造函數(shù)如下:

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

當(dāng)使用User::with('phone')->get()的時(shí)候,就會(huì)去eager load進(jìn)phone了泻骤,具體的過(guò)程中漆羔,在調(diào)用Eloquent\Builder的get的時(shí)候梧奢,里面有個(gè)邏輯是:

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

獲取has one關(guān)系,我們跟著看到代碼演痒,會(huì)調(diào)用到函數(shù)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會(huì)調(diào)用到User()->phone(),即此處$instanceHasOne嫡霞,接著調(diào)用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;
}

最后調(diào)用match方法希柿,就是正確的給每個(gè)model設(shè)置好relation關(guān)系诊沪。

以上就是我們分析的HasOne的實(shí)現(xiàn),其他的關(guān)系都類(lèi)似曾撤,此處不再重復(fù)端姚,然后eager load的含義是指,當(dāng)我們要加載多個(gè)數(shù)據(jù)的時(shí)候挤悉,我們盡可能用一條sql解決渐裸,而不是多條sql,具體來(lái)說(shuō)如果我們有多個(gè)Users装悲,需要加載Phones的昏鹃,如果不采用eager,在每個(gè)sql就是where user_id=?诀诊,而eager模式則是where user_id in (?,?,?)洞渤,這樣差異就很明顯了.

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

講到這,我們列舉下對(duì)象之間的關(guān)系

One-To-One

User 和 Phone的1對(duì)1的關(guān)系属瓣,

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的查詢類(lèi)似于下面

select id from phone where user_id in (1)

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

One-To-Many

以Post和Comment為例载迄,一個(gè)Post會(huì)有多個(gè)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

以u(píng)ser和role為例,一個(gè)用戶會(huì)有不同的角色抡蛙,一個(gè)角色也會(huì)有不同的人护昧,這個(gè)時(shí)候就需要一張中間表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');
    }
}

這個(gè)關(guān)系我們稍微具體講下粗截,我們?cè)谑褂蒙峡赡軙?huì)是下面這樣子的

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

在構(gòu)造函數(shù)中惋耙,會(huì)調(diào)用addConstraints方法,如下

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

此處會(huì)預(yù)先設(shè)置setSelect()->setJoin()->setWhere()熊昌,作用分別是:

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

setJoin():新增join绽榛, join user_role on role.id = user_role.role_id,聯(lián)合查詢

setWhere():新增 user_id = ?

查詢的表是role浴捆,join表user_role

在get的時(shí)候蒜田,其邏輯和HasOne等關(guān)系也所有不同,代碼如下:

// 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);
}

此處有個(gè)方法叫hydratePivotRelation选泻,我們進(jìn)入看下到底是怎么回事

// class belongsToMany
protected function hydratePivotRelation(array $models)
{
 // 將中間記錄取出來(lái)冲粤,設(shè)置屬性pivot為Model pivot
  foreach ($models as $model)
  {
    $values = $this->cleanPivotAttributes($model);

    $pivot = $this->newExistingPivot($values);

    $model->setRelation('pivot', $pivot);
  }
}

其實(shí)做的事情就是設(shè)置了Role的pivot屬性美莫。

到這,我們就分析完了eloquent在f6e2170版本上具有的功能了梯捕,到目前為止厢呵,eloquent的類(lèi)圖如下:

總結(jié)

目前,我們分析到的版本是f6e2170傀顾,已經(jīng)具備了一個(gè)orm該需要的功能了襟铭,Connection負(fù)責(zé)數(shù)據(jù)庫(kù)操作,Builder負(fù)責(zé)面向?qū)ο蟮膕ql操作短曾,Grammar負(fù)責(zé)sql的拼裝寒砖,Eloquent/Model是Active Record模式的核心Model,同時(shí)具備領(lǐng)域邏輯和數(shù)據(jù)庫(kù)操作功能嫉拐,其中數(shù)據(jù)庫(kù)操作功能是委托給了Eloquent/Builder哩都,同時(shí)我們也定義了對(duì)象的3種關(guān)系,1-1婉徘,1-N漠嵌,N-N,下一階段盖呼,Eloquent將會(huì)實(shí)現(xiàn)migrations or database modification logic的功能儒鹿,盡情期待。

這是orm的第二篇几晤,你的鼓勵(lì)是我繼續(xù)寫(xiě)下去的動(dòng)力约炎,期待我們共同進(jìn)步。

這個(gè)時(shí)代锌仅,每個(gè)人都是超級(jí)個(gè)體章钾!關(guān)注我,一起成長(zhǎng)热芹!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贱傀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子伊脓,更是在濱河造成了極大的恐慌府寒,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件报腔,死亡現(xiàn)場(chǎng)離奇詭異株搔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)纯蛾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)纤房,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人翻诉,你說(shuō)我怎么就攤上這事炮姨“乒危” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵舒岸,是天一觀的道長(zhǎng)绅作。 經(jīng)常有香客問(wèn)我,道長(zhǎng)蛾派,這世上最難降的妖魔是什么俄认? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮洪乍,結(jié)果婚禮上眯杏,老公的妹妹穿的比我還像新娘。我一直安慰自己典尾,他們只是感情好役拴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布糊探。 她就那樣靜靜地躺著钾埂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪科平。 梳的紋絲不亂的頭發(fā)上褥紫,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音瞪慧,去河邊找鬼髓考。 笑死,一個(gè)胖子當(dāng)著我的面吹牛弃酌,可吹牛的內(nèi)容都是我干的氨菇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼妓湘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼查蓉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起榜贴,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤豌研,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后唬党,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鹃共,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年驶拱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了霜浴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蓝纲,死狀恐怖阴孟,靈堂內(nèi)的尸體忽然破棺而出荸百,到底是詐尸還是另有隱情,我是刑警寧澤徽千,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布褒纲,位于F島的核電站,受9級(jí)特大地震影響类溢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜闯冷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一蛇耀、第九天 我趴在偏房一處隱蔽的房頂上張望辩诞。 院中可真熱鬧,春花似錦纺涤、人聲如沸译暂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拧咳。三九已至伯顶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間骆膝,已是汗流浹背祭衩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工掐暮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留愉择,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓衷戈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親殖妇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子破花,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容