上一篇文章咱們講了Database的查詢構建器QueryBuilder, 學習了QueryBuilder爲構建生成SQL語句而提供的Fluent Api的代碼實現。這一篇文章咱們來學習Laravel Database裏另一個重要的部分: Eloquent Model。git
Eloquent Model把數據表的屬性、關聯關係等抽象到了每一個Model類中,因此Model類是對數據表的抽象,而Model對象則是對錶中單條記錄的抽象。Eloquent Model以上文講到的QueryBuilder爲基礎提供了Eloquent Builder與數據庫進行交互,此外還提供了模型關聯優雅的解決了多個數據表之間的關聯關係。github
Eloquent Builder是在上文說到的Query Builder的基礎上實現的,咱們仍是經過具體的例子來看,上文用到的:數據庫
DB::table('user')->where('name', 'James')->where('age', 27)->get();
把它改寫爲使用Model的方式後就變成了數組
User::where('name', 'James')->where('age', 27)->get();
在Model類文件裏咱們並無找到where
、find
、first
這些經常使用的查詢方法,咱們都知道當調用一個不存在的類方法時PHP會觸發魔術方法__callStatic
, 調用不存在的實例方法會觸發__call
, 很容易就猜到上面這些方法就是經過這兩個魔術方法來動態調用的,下面讓咱們看一下源碼。app
namespace Illuminate\Database\Eloquent; abstract class Model implements ... { public function __call($method, $parameters) { if (in_array($method, ['increment', 'decrement'])) { return $this->$method(...$parameters); } return $this->newQuery()->$method(...$parameters); } public static function __callStatic($method, $parameters) { return (new static)->$method(...$parameters); } // new Eloquent Builder public function newQuery() { return $this->registerGlobalScopes($this->newQueryWithoutScopes()); } public function newQueryWithoutScopes() { $builder = $this->newEloquentBuilder($this->newBaseQueryBuilder()); //設置builder的Model實例,這樣在構建和執行query時就能使用model中的信息了 return $builder->setModel($this) ->with($this->with) ->withCount($this->withCount); } //建立數據庫鏈接的QueryBuilder protected function newBaseQueryBuilder() { $connection = $this->getConnection(); return new QueryBuilder( $connection, $connection->getQueryGrammar(), $connection->getPostProcessor() ); } }
經過上面的那些代碼咱們能夠看到對Model調用的這些查詢相關的方法最後都會經過__call
轉而去調用Eloquent Builder實例的這些方法,咱們看到實例化的時候把數據庫鏈接的QueryBuilder對象傳給了Eloquent Builder的構造方法, 咱們去看一下Eloquent Builder的源碼。學習
namespace Illuminate\Database\Eloquent; class Builder { public function __construct(QueryBuilder $query) { $this->query = $query; } public function where($column, $operator = null, $value = null, $boolean = 'and') { if ($column instanceof Closure) { $query = $this->model->newQueryWithoutScopes(); $column($query); $this->query->addNestedWhereQuery($query->getQuery(), $boolean); } else { $this->query->where(...func_get_args()); } return $this; } public function get($columns = ['*']) { $builder = $this->applyScopes(); //若是獲取到了model還會load要預加載的模型關聯,避免運行n+1次查詢 if (count($models = $builder->getModels($columns)) > 0) { $models = $builder->eagerLoadRelations($models); } return $builder->getModel()->newCollection($models); } public function getModels($columns = ['*']) { return $this->model->hydrate( $this->query->get($columns)->all() )->all(); } //將查詢出來的結果轉換成Model對象組成的Collection public function hydrate(array $items) { //新建一個model實例 $instance = $this->newModelInstance(); return $instance->newCollection(array_map(function ($item) use ($instance) { return $instance->newFromBuilder($item); }, $items)); } //first 方法就是應用limit 1,get返回的集合後用Arr::first()從集合中取出model對象 public function first($columns = ['*']) { return $this->take(1)->get($columns)->first(); } } //newModelInstance newFromBuilder 定義在\Illuminate\Database\EloquentModel類文件裏 public function newFromBuilder($attributes = [], $connection = null) { //新建實例,而且把它的exists屬性設成true, save時會根據這個屬性判斷是insert仍是update $model = $this->newInstance([], true); $model->setRawAttributes((array) $attributes, true); $model->setConnection($connection ?: $this->getConnectionName()); $model->fireModelEvent('retrieved', false); return $model; }
代碼裏Eloquent Builder的where方法在接到調用請求後直接把請求轉給來QueryBuilder的where方法,而後get方法也是先經過QueryBuilder的get方法執行查詢拿到結果數組後再經過newFromBuilder
方法把結果數組轉換成Model對象構成的集合,而另一個比較經常使用的方法first
也是在get
方法的基礎上實現的,對query應用limit 1,再從get
方法返回的集合中用 Arr::first()
取出model對象返回給調用者。ui
看完了Model查詢的實現咱們再來看一下update、create和delete的實現,仍是從一開始的查詢例子繼續擴展:this
$user = User::where('name', 'James')->where('age', 27)->first();
如今經過Model查詢咱們獲取裏一個User Model的實例,咱們如今要把這個用戶的age改爲28歲:spa
$user->age = 28; $user->save();
咱們知道model的屬性對應的是數據表的字段,在上面get方法返回Model實例集合時咱們看到過把數據記錄的字段和字段值都賦值給了Model實例的$attributes屬性, Model實例訪問和設置這些字段對應的屬性時是經過__get
和__set
魔術方法動態獲取和設置這些屬性值的。code
abstract class Model implements ... { public function __get($key) { return $this->getAttribute($key); } public function __set($key, $value) { $this->setAttribute($key, $value); } public function getAttribute($key) { if (! $key) { return; } //若是attributes數組的index裏有$key或者$key對應一個屬性訪問器`'get' . $key` 則從這裏取出$key對應的值 //不然就嘗試去獲取模型關聯的值 if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) { return $this->getAttributeValue($key); } if (method_exists(self::class, $key)) { return; } //獲取模型關聯的值 return $this->getRelationValue($key); } public function getAttributeValue($key) { $value = $this->getAttributeFromArray($key); if ($this->hasGetMutator($key)) { return $this->mutateAttribute($key, $value); } if ($this->hasCast($key)) { return $this->castAttribute($key, $value); } if (in_array($key, $this->getDates()) && ! is_null($value)) { return $this->asDateTime($value); } return $value; } protected function getAttributeFromArray($key) { if (isset($this->attributes[$key])) { return $this->attributes[$key]; } } public function setAttribute($key, $value) { if ($this->hasSetMutator($key)) { $method = 'set'.Str::studly($key).'Attribute'; return $this->{$method}($value); } elseif ($value && $this->isDateAttribute($key)) { $value = $this->fromDateTime($value); } if ($this->isJsonCastable($key) && ! is_null($value)) { $value = $this->castAttributeAsJson($key, $value); } if (Str::contains($key, '->')) { return $this->fillJsonAttribute($key, $value); } $this->attributes[$key] = $value; return $this; } }
因此當執行$user->age = 28
時, User Model實例裏$attributes屬性會變成
protected $attributes = [ ... 'age' => 28, ... ]
設置好屬性新的值以後執行Eloquent Model的save
方法就會更新數據庫裏對應的記錄,下面咱們看看save
方法裏的邏輯:
abstract class Model implements ... { public function save(array $options = []) { $query = $this->newQueryWithoutScopes(); if ($this->fireModelEvent('saving') === false) { return false; } //查詢出來的Model實例的exists屬性都是true if ($this->exists) { $saved = $this->isDirty() ? $this->performUpdate($query) : true; } else { $saved = $this->performInsert($query); if (! $this->getConnectionName() && $connection = $query->getConnection()) { $this->setConnection($connection->getName()); } } if ($saved) { $this->finishSave($options); } return $saved; } //判斷對字段是否有更改 public function isDirty($attributes = null) { return $this->hasChanges( $this->getDirty(), is_array($attributes) ? $attributes : func_get_args() ); } //數據表字段會保存在$attributes和$original兩個屬性裏,update前要找出被更改的字段 public function getDirty() { $dirty = []; foreach ($this->getAttributes() as $key => $value) { if (! $this->originalIsEquivalent($key, $value)) { $dirty[$key] = $value; } } return $dirty; } protected function performUpdate(Builder $query) { if ($this->fireModelEvent('updating') === false) { return false; } if ($this->usesTimestamps()) { $this->updateTimestamps(); } $dirty = $this->getDirty(); if (count($dirty) > 0) { $this->setKeysForSaveQuery($query)->update($dirty); $this->fireModelEvent('updated', false); $this->syncChanges(); } return true; } //爲查詢設置where primary key = xxx protected function setKeysForSaveQuery(Builder $query) { $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); return $query; } }
在save裏會根據Model實例的exists
屬性來判斷是執行update仍是insert, 這裏咱們用的這個例子是update,在update時程序找出$attributes
和$original
兩個數組屬性的差集(獲取Model對象時會把數據表字段會保存在$attributes
和$original
兩個屬性),若是沒有更改那麼update到這裏就結束了,有更改那麼就繼續去執行performUpdate
方法,performUpdate
方法會執行Eloquent Builder的update方法, 而Eloquent Builder依賴的仍是數據庫鏈接的Query Builder實例去最後執行的數據庫update。
剛纔說經過Eloquent Model獲取模型時(在newFromBuilder
方法裏)會把Model實例的exists
屬性設置爲true,那麼對於新建的Model實例這個屬性的值是false,在執行save
方法時就會去執行performInsert
方法
protected function performInsert(Builder $query) { if ($this->fireModelEvent('creating') === false) { return false; } //設置created_at和updated_at屬性 if ($this->usesTimestamps()) { $this->updateTimestamps(); } $attributes = $this->attributes; //若是表的主鍵自增insert數據並把新記錄的id設置到屬性裏 if ($this->getIncrementing()) { $this->insertAndSetId($query, $attributes); } //不然直接簡單的insert else { if (empty($attributes)) { return true; } $query->insert($attributes); } // 把exists設置成true, 下次在save就會去執行update了 $this->exists = true; $this->wasRecentlyCreated = true; //觸發created事件 $this->fireModelEvent('created', false); return true; }
performInsert
裏若是表是主鍵自增的,那麼在insert後會設置新記錄主鍵ID的值到Model實例的屬性裏,同時還會幫咱們維護時間字段和exists
屬性。
Eloquent Model的delete操做也是同樣, 經過Eloquent Builder去執行數據庫鏈接的QueryBuilder裏的delete方法刪除數據庫記錄:
//Eloquent Model public function delete() { if (is_null($this->getKeyName())) { throw new Exception('No primary key defined on model.'); } if (! $this->exists) { return; } if ($this->fireModelEvent('deleting') === false) { return false; } $this->touchOwners(); $this->performDeleteOnModel(); $this->fireModelEvent('deleted', false); return true; } protected function performDeleteOnModel() { $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete(); $this->exists = false; } //Eloquent Builder public function delete() { if (isset($this->onDelete)) { return call_user_func($this->onDelete, $this); } return $this->toBase()->delete(); } //Query Builder public function delete($id = null) { if (! is_null($id)) { $this->where($this->from.'.id', '=', $id); } return $this->connection->delete( $this->grammar->compileDelete($this), $this->cleanBindings( $this->grammar->prepareBindingsForDelete($this->bindings) ) ); }
Query Builder的實現細節咱們在上一篇文章裏已經說過了這裏再也不贅述,若是好奇Query Builder是怎麼執行SQL操做的能夠回去翻看上一篇文章。
本文咱們詳細地看了Eloquent Model是怎麼執行CRUD的,就像開頭說的Eloquent Model經過Eloquent Builder來完成數據庫操做,而Eloquent Builder是在Query Builder的基礎上作了進一步封裝, Eloquent Builder會方法調用轉給Query Builder裏對應的方法來完成操做,因此在Query Builder裏能使用的方法到Eloquent Model中一樣都能使用。
除了對數據表、基本的CRUD的抽象外,模型另外的一個重要的特色是模型關聯,它幫助咱們優雅的解決了數據表間的關聯關係。咱們在以後的文章再來詳細看模型關聯部分的實現。
本文已經收錄在系列文章Laravel源碼學習裏,歡迎訪問閱讀。