orm 系列 之 Eloquent演化歷程1

hjim2904 8年前發布 | 7K 次閱讀 數據持久化 SQL

Eloquent是laravel中的orm,采取的是active record的設計模式,里面的對象不僅包括領域邏輯,還包括了數據庫操作,但是大家平時使用的時候可能沒有探究eloquent是怎么設計的,active record這種模式的優缺點等問題,下面我會帶領大家從頭開始看看Eloquent是如何設計并實現的。

本文是orm系列的第二篇,也是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 查看。

useIlluminate\Database\Query\BuilderasBaseBuilder;

classBuilderextendsBaseBuilder{
/**
 * The model being queried.
 *
 * @varIlluminate\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的關系,看代碼

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

 $instance = new$related;

returnnewHasOne($instance->newQuery(),$this, $foreignKey);
}

我們看到當調用Model的 hasOne 方法后,返回是一個HasOne,即Relation,當我們調用Relation的方法時,是怎么處理的呢?通過魔術方法 __call ,將其委托給了 Eloquent\Builder ,

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.");
}

即其實Relation是對 Eloquent\Builder 的一個封裝,支持面向對象式的sql操作,我們下面來看下當我們使用HasOne的時候發生了什么。

假設我們有個User,Phone,然后User和Phone的關系是HasOne,在User聲明上就會有

classUserextendsModel
{
/**
 * Get the phone record associated with the user.
 */
publicfunctionphone()
 {
return$this->hasOne('App\Phone');
 }
}

此時HasOne的構造函數如下:

// builder是Eloquent\Builder, parent是Uer,$foreign_key是user_id
$relation = newHasOne($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 ,具體看代碼:

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

其中 getRelation 會調用到 User()->phone() ,即此處 $instance 是 HasOne ,接著調用 HasOne->addEagerConstraints() 和 HasOne->initializeRelation() ,具體的代碼是:

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

最后調用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的關系,

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

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

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

此處的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 ,代碼聲明上如下:

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

這個關系我們稍微具體講下,我們在使用上可能會是下面這樣子的

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

在構造函數中,會調用 addConstraints 方法,如下

// class belongsToMany
publicfunctionaddConstraints()
{
$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
publicfunctionget($columns = array('*'))
{
 $models = $this->query->getModels($columns);

$this->hydratePivotRelation($models);

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

returnnewCollection($models);
}

此處有個方法叫 hydratePivotRelation ,我們進入看下到底是怎么回事

// class belongsToMany
protectedfunctionhydratePivotRelation(array $models)
{
// 將中間記錄取出來,設置屬性pivot為Model pivot
foreach($modelsas$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 的功能,盡情期待。

 

來自:http://blog.zhuanxu.org/2016-11-24-eloquent-1.html

 

 本文由用戶 hjim2904 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!