Laravel核心程式碼學習 — Model增刪改查底層實現

kevinyan發表於2019-03-04

上篇文章我們講了Database的查詢構建器Query Builder, 學習了Query Builder為構建生成SQL語句而提供的Fluent Api的程式碼實現。這篇文章我們來學習Laravel Database地另外一個重要的部分: Eloquent Model。

Eloquent Model把資料表的屬性、關聯關係等抽象到了每個Model類中,所以Model類是對資料表的抽象,而Model物件則是對錶中單條記錄的抽象。Eloquent Model以上文講到的Query Builder為基礎提供了Eloquent Builder與資料庫進行互動,此外還提供了模型關聯優雅地解決了多個資料表之間的關聯關係。

載入Eloquent Builder

Eloquent Builder是在上文說到的Query Builder的基礎上實現的,我們還是通過具體的例子來看,上文用到的:

DB::table(`user`)->where(`name`, `James`)->where(`age`, 27)->get();
複製程式碼

把它改寫為使用Model的方式後就變成了

User::where(`name`, `James`)->where(`age`, 27)->get();
複製程式碼

在Model類檔案裡我們並沒有找到wherefindfirst這些常用的查詢方法,我們都知道當呼叫一個不存在的類方法時PHP會觸發魔術方法__callStatic, 呼叫不存在的例項方法會觸發__call, 很容易就猜到上面這些方法就是通過這兩個魔術方法來動態呼叫的,下面讓我們看一下原始碼。

namespace IlluminateDatabaseEloquent;
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查詢

通過上面的那些程式碼我們可以看到對Model呼叫的這些查詢相關的方法最後都會通過__call轉而去呼叫Eloquent Builder例項的這些方法,Eloquent Builder與底層資料庫互動的部分都是依賴Query Builder來實現的,我們看到在例項化Eloquent Builder的時候把資料庫連線的QueryBuilder物件傳給了它的構造方法, 下面就去看一下Eloquent Builder的原始碼。

namespace IlluminateDatabaseEloquent;
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 定義在IlluminateDatabaseEloquentModel類檔案裡

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方法在接到呼叫請求後直接把請求轉給來Query Builder的where方法,然後get方法也是先通過Query Builder的get方法執行查詢拿到結果陣列後再通過newFromBuilder方法把結果陣列轉換成Model物件構成的集合,而另外一個比較常用的方法first也是在get方法的基礎上實現的,對query應用limit 1,再從get方法返回的集合中用 Arr::first()取出model物件返回給呼叫者。

Model更新

看完了Model查詢的實現我們再來看一下update、create和delete的實現,還是從一開始的查詢例子繼續擴充套件:

$user = User::where(`name`, `James`)->where(`age`, 27)->first();
複製程式碼

現在通過Model查詢我們獲取裡一個User Model的例項,我們現在要把這個使用者的age改成28歲:

$user->age = 28;
$user->save();
複製程式碼

我們知道model的屬性對應的是資料表的欄位,在上面get方法返回Model例項集合時我們看到過把資料記錄的欄位和欄位值都賦值給了Model例項的$attributes屬性, Model例項訪問和設定這些欄位對應的屬性時是通過__get__set魔術方法動態獲取和設定這些屬性值的。

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 . `Attribute`` 則從這裡取出$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)
    {
    	//如果$key存在屬性修改器則去呼叫$key地屬性修改器``set` . $key . `Attribute`` 比如`setNameAttribute`
        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;
    }	
}
複製程式碼

如果Model定義的屬性修改器那麼在設定屬性的時候會去執行修改器,在我們的例子中並沒有用到屬性修改器。當執行$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兩個array屬性裡各欄位的欄位值找被更改的欄位(獲取Model物件時會把資料表欄位會儲存在$attributes$original兩個屬性),如果沒有被更改的欄位那麼update到這裡就結束了,有更改那麼就繼續去執行performUpdate方法,performUpdate方法會執行Eloquent Builder的update方法, 而Eloquent Builder依賴的還是資料庫連線的Query Builder例項去最後執行的資料庫update。

Model寫入

剛才說通過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屬性。

Model刪除

Eloquent Model的delete操作也是一樣, 通過Eloquent Builder去執行資料庫連線的Query Builder裡的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會把這些CRUD方法的呼叫轉給Query Builder裡對應的方法來完成操作,所以在Query Builder裡能使用的方法到Eloquent Model中同樣都能使用。

除了對資料表、基本的CRUD的抽象外,模型另外的一個重要的特點是模型關聯,它幫助我們優雅的解決了資料表間的關聯關係。我們在之後的文章再來詳細看模型關聯部分的實現。

本文已經收錄在系列文章Laravel核心程式碼學習裡,歡迎訪問閱讀。

相關文章