orm 系列 之 Eloquent演化歷程1
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