Laravel Database——Eloquent Model 原始碼分析(下)

leoyang發表於2017-10-10

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis

獲取模型

get 函式

public function get($columns = ['*'])
{
    $builder = $this->applyScopes();

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

get 函式會將 QueryBuilder 所獲取的資料進一步包裝 hydratehydrate 函式會將資料庫取回來的資料打包成資料庫模型物件 Eloquent Model,如果可以獲取到資料,還會利用函式 eagerLoadRelations 來預載入關係模型。

public function hydrate(array $items)
{
    $instance = $this->newModelInstance();

    return $instance->newCollection(array_map(function ($item) use ($instance) {
        return $instance->newFromBuilder($item);
    }, $items));
}

newModelInstance 函式建立了一個新的資料庫模型物件,重要的是這個函式為新的資料庫模型物件賦予了 connection

public function newModelInstance($attributes = [])
{
    return $this->model->newInstance($attributes)->setConnection(
        $this->query->getConnection()->getName()
    );
}

newFromBuilder 函式會將所有資料庫資料存入另一個新的 Eloquent Modelattributes 中:

public function newFromBuilder($attributes = [], $connection = null)
{
    $model = $this->newInstance([], true);

    $model->setRawAttributes((array) $attributes, true);

    $model->setConnection($connection ?: $this->getConnectionName());

    $model->fireModelEvent('retrieved', false);

    return $model;
}

newInstance 函式專用於建立新的資料庫物件模型:

public function newInstance($attributes = [], $exists = false)
{
    $model = new static((array) $attributes);

    $model->exists = $exists;

    $model->setConnection(
        $this->getConnectionName()
    );

    return $model;
}

值得注意的是 newInstanceexist 設定為 true,意味著當前這個資料庫模型物件是從資料庫中獲取而來,並非是手動新建的,這個 exist 為真,我們才能對這個資料庫物件進行 update

setRawAttributes 函式為新的資料庫物件賦予屬性值,並且進行 sync,標誌著物件的原始狀態:

public function setRawAttributes(array $attributes, $sync = false)
{
    $this->attributes = $attributes;

    if ($sync) {
        $this->syncOriginal();
    }

    return $this;
}

public function syncOriginal()
{
    $this->original = $this->attributes;

    return $this;
}

這個原始狀態的記錄十分重要,原因是 save 函式就是利用原始值 original 與屬性值 attributes 的差異來決定更新的欄位。

find 函式

find 函式用於利用主鍵 id 來查詢資料,find 函式也可以傳入陣列,查詢多個資料

public function find($id, $columns = ['*'])
{
    if (is_array($id) || $id instanceof Arrayable) {
        return $this->findMany($id, $columns);
    }

    return $this->whereKey($id)->first($columns);
}

public function findMany($ids, $columns = ['*'])
{
    if (empty($ids)) {
        return $this->model->newCollection();
    }

    return $this->whereKey($ids)->get($columns);
}

findOrFail

laravel 還提供 findOrFail 函式,一般用於 controller,在未找到記錄的時候會丟擲異常。

public function findOrFail($id, $columns = ['*'])
{
    $result = $this->find($id, $columns);

    if (is_array($id)) {
        if (count($result) == count(array_unique($id))) {
            return $result;
        }
    } elseif (! is_null($result)) {
        return $result;
    }

    throw (new ModelNotFoundException)->setModel(
        get_class($this->model), $id
    );
}

其他查詢與資料獲取方法

所用 Query Builder 支援的查詢方法,例如 selectselectSubwhereDatewhereBetween 等等,都可以直接對 Eloquent Model 直接使用,程式會通過魔術方法呼叫 Query Builder 的相關方法:

protected $passthru = [
    'insert', 'insertGetId', 'getBindings', 'toSql',
    'exists', 'count', 'min', 'max', 'avg', 'sum', 'getConnection',
];

public function __call($method, $parameters)
{
    ...

    if (in_array($method, $this->passthru)) {
        return $this->toBase()->{$method}(...$parameters);
    }

    $this->query->{$method}(...$parameters);

    return $this;
}

passthru 中的各個函式在呼叫前需要載入查詢作用域,原因是這些操作基本上是 aggregate 的,需要新增搜尋條件才能更加符合預期:

public function toBase()
{
    return $this->applyScopes()->getQuery();
}

 

新增和更新模型

save 函式

Eloquent Model 中,新增與更新模型可以統一用 save 函式。在新增模型的時候需要事先為 model 屬性賦值,可以單個手動賦值,也可以批量賦值。在更新模型的時候,需要事先從資料庫中取出模型,然後修改模型屬性,最後執行 save 更新操作。官方文件:新增和更新模型

public function save(array $options = [])
{
    $query = $this->newQueryWithoutScopes();

    if ($this->fireModelEvent('saving') === false) {
        return false;
    }

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

save 函式不會載入全域性作用域,原因是凡是利用 save 函式進行的插入或者更新的操作都不會存在 where 條件,僅僅利用自身的主鍵屬性來進行更新。如果需要 where 條件可以使用 query\builderupdate 函式,我們在下面會詳細介紹:

public function newQueryWithoutScopes()
{
    $builder = $this->newEloquentBuilder($this->newBaseQueryBuilder());

    return $builder->setModel($this)
                ->with($this->with)
                ->withCount($this->withCount);
}

protected function newBaseQueryBuilder()
{
    $connection = $this->getConnection();

    return new QueryBuilder(
        $connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
    );
}

newQueryWithoutScopes 函式建立新的沒有任何其他條件的 Eloquent\builder 類,而 Eloquent\builder 類需要 Query\builder 類作為底層查詢構造器。

performUpdate 函式

如果當前的資料庫模型物件是從資料庫中取出的,也就是直接或間接的呼叫 get() 函式從資料庫中獲取到的資料庫物件,那麼其 exists 必然是 true

public function isDirty($attributes = null)
{
    return $this->hasChanges(
        $this->getDirty(), is_array($attributes) ? $attributes : func_get_args()
    );
}

public function getDirty()
{
    $dirty = [];

    foreach ($this->getAttributes() as $key => $value) {
        if (! $this->originalIsEquivalent($key, $value)) {
            $dirty[$key] = $value;
        }
    }

    return $dirty;
}

getDirty 函式可以獲取所有與原始值不同的屬性值,也就是需要更新的資料庫欄位。關鍵函式在於 originalIsEquivalent

protected function originalIsEquivalent($key, $current)
{
    if (! array_key_exists($key, $this->original)) {
        return false;
    }

    $original = $this->getOriginal($key);

    if ($current === $original) {
        return true;
    } elseif (is_null($current)) {
        return false;
    } elseif ($this->isDateAttribute($key)) {
        return $this->fromDateTime($current) ===
               $this->fromDateTime($original);
    } elseif ($this->hasCast($key)) {
        return $this->castAttribute($key, $current) ===
               $this->castAttribute($key, $original);
    }

    return is_numeric($current) && is_numeric($original)
            && strcmp((string) $current, (string) $original) === 0;
}

可以看到,對於資料庫可以轉化的屬性都要先進行轉化,然後再開始對比。比較出的結果,就是我們需要 update 的欄位。

執行更新的時候,除了 getDirty 函式獲得的待更新欄位,還會有 UPDATED_AT 這個欄位:

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

protected function updateTimestamps()
{
    $time = $this->freshTimestamp();

    if (! is_null(static::UPDATED_AT) && ! $this->isDirty(static::UPDATED_AT)) {
        $this->setUpdatedAt($time);
    }

    if (! $this->exists && ! $this->isDirty(static::CREATED_AT)) {
        $this->setCreatedAt($time);
    }
}

執行更新的時候,where 條件只有一個,那就是主鍵 id

protected function setKeysForSaveQuery(Builder $query)
{
    $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());

    return $query;
}

protected function getKeyForSaveQuery()
{
    return $this->original[$this->getKeyName()]
                    ?? $this->getKey();
}

public function getKey()
{
    return $this->getAttribute($this->getKeyName());
}

最後會呼叫 EloquentBuilderupdate 函式:

public function update(array $values)
{
    return $this->toBase()->update($this->addUpdatedAtColumn($values));
}

protected function addUpdatedAtColumn(array $values)
{
    if (! $this->model->usesTimestamps()) {
        return $values;
    }

    return Arr::add(
        $values, $this->model->getUpdatedAtColumn(),
        $this->model->freshTimestampString()
    );
}

public function freshTimestampString()
{
    return $this->fromDateTime($this->freshTimestamp());
}

public function fromDateTime($value)
{
    return is_null($value) ? $value : $this->asDateTime($value)->format(
        $this->getDateFormat()
    );
}

performInsert

關於資料庫物件的插入,如果資料庫的主鍵被設定為 increment,也就是自增的話,程式會呼叫 insertAndSetId,這個時候不需要給資料庫模型物件手動賦值主鍵 id。若果資料庫的主鍵並不支援自增,那麼就需要在插入前,為資料庫物件的主鍵 id 賦值,否則資料庫會報錯。

protected function performInsert(Builder $query)
{
    if ($this->fireModelEvent('creating') === false) {
        return false;
    }

    if ($this->usesTimestamps()) {
        $this->updateTimestamps();
    }

    $attributes = $this->attributes;

    if ($this->getIncrementing()) {
        $this->insertAndSetId($query, $attributes);
    }
    else {
        if (empty($attributes)) {
            return true;
        }

        $query->insert($attributes);
    }

    $this->exists = true;

    $this->wasRecentlyCreated = true;

    $this->fireModelEvent('created', false);

    return true;
}

laravel 預設資料庫的主鍵支援自增屬性,程式呼叫的也是函式 insertAndSetId 函式:

protected function insertAndSetId(Builder $query, $attributes)
{
    $id = $query->insertGetId($attributes, $keyName = $this->getKeyName());

    $this->setAttribute($keyName, $id);
}

插入後,會將插入後得到的主鍵 id 返回,並賦值到模型的屬性當中。

如果資料庫主鍵不支援自增,那麼我們在資料庫類中要設定:

public $incrementing = false;

每次進行插入資料的時候,需要手動給主鍵賦值。

update 函式

save 函式僅僅支援手動的屬性賦值,無法批量賦值。laravelEloquent Model 還有一個函式: update 支援批量屬性賦值。有意思的是,Eloquent Builder 也有函式 update,那個是上一小節提到的 performUpdate 所呼叫的函式。

兩個 update 功能一致,只是 Modelupdate 函式比較適用於更新從資料庫取回的資料庫物件:

$flight = App\Flight::find(1);

$flight->update(['name' => 'New Flight Name','desc' => 'test']);

Builderupdate 適用於多查詢條件下的更新:

App\Flight::where('active', 1)
          ->where('destination', 'San Diego')
          ->update(['delayed' => 1]);

無論哪一種,都會自動更新 updated_at 欄位。

Modelupdate 函式借助 fill 函式與 save 函式:

public function update(array $attributes = [], array $options = [])
{
    if (! $this->exists) {
        return false;
    }

    return $this->fill($attributes)->save($options);
}

make 函式

同樣的,save 的插入也僅僅支援手動屬性賦值,如果想實現批量屬性賦值的插入可以使用 make 函式:

$model = App\Flight::make(['name' => 'New Flight Name','desc' => 'test']);

$model->save();

make 函式實際上僅僅是新建了一個 Eloquent Model,並批量賦予屬性值:

public function make(array $attributes = [])
{
    return $this->newModelInstance($attributes);
}

public function newModelInstance($attributes = [])
{
    return $this->model->newInstance($attributes)->setConnection(
        $this->query->getConnection()->getName()
    );
}

create 函式

如果想要一步到位,批量賦值屬性與插入一起操作,可以使用 create 函式:

App\Flight::create(['name' => 'New Flight Name','desc' => 'test']);

相比較 make 函式,create 函式更進一步呼叫了 save 函式:

public function create(array $attributes = [])
{
    return tap($this->newModelInstance($attributes), function ($instance) {
        $instance->save();
    });
}

實際上,屬性值是否可以批量賦值需要受 fillableguarded 來控制,如果我們想要強制批量賦值可以使用 forceCreate

public function forceCreate(array $attributes)
{
    return $this->model->unguarded(function () use ($attributes) {
        return $this->newModelInstance()->create($attributes);
    });
}

findOrNew 函式

laravel 提供一種主鍵查詢或者新建資料庫物件的函式:findOrNew

public function findOrNew($id, $columns = ['*'])
{
    if (! is_null($model = $this->find($id, $columns))) {
        return $model;
    }

    return $this->newModelInstance();
}

值得注意的是,當查詢失敗的時候,會返回一個全新的資料庫物件,不含有任何 attributes

firstOrNew 函式

laravel 提供一種自定義查詢或者新建資料庫物件的函式:firstOrNew

public function firstOrNew(array $attributes, array $values = [])
{
    if (! is_null($instance = $this->where($attributes)->first())) {
        return $instance;
    }

    return $this->newModelInstance($attributes + $values);
}

值得注意的是,如果查詢失敗,會返回一個含有 attributesvalues 兩者合併的屬性的資料庫物件。

firstOrCreate 函式

類似於 firstOrNew 函式,firstOrCreate 函式也用於自定義查詢或者新建資料庫物件,不同的是,firstOrCreate 函式還進一步對資料進行了插入操作:

public function firstOrCreate(array $attributes, array $values = [])
{
    if (! is_null($instance = $this->where($attributes)->first())) {
        return $instance;
    }

    return tap($this->newModelInstance($attributes + $values), function ($instance) {
        $instance->save();
    });
}

updateOrCreate 函式

firstOrCreate 函式基礎上,除了對資料進行查詢,還會對查詢成功的資料利用 value 進行更新:

public function updateOrCreate(array $attributes, array $values = [])
{
    return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
        $instance->fill($values)->save();
    });
}

firstOr 函式

如果想要自定義查詢失敗後的操作,可以使用 firstOr 函式,該函式可以傳入閉包函式,處理找不到資料的情況:

public function firstOr($columns = ['*'], Closure $callback = null)
{
    if ($columns instanceof Closure) {
        $callback = $columns;

        $columns = ['*'];
    }

    if (! is_null($model = $this->first($columns))) {
        return $model;
    }

    return call_user_func($callback);
}

 

刪除模型

刪除模型也會分為兩種,一種是針對 Eloquent Model 的刪除,這種刪除必須是從資料庫中取出的物件。還有一種是 Eloquent Builder 的刪除,這種刪除一般會帶有多個查詢條件。我們這一小節主要講 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;
}

刪除模型時,模型物件必然要有主鍵。performDeleteOnModel 函式執行具體的刪除操作:

protected function performDeleteOnModel()
{
    $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete();

    $this->exists = false;
}

protected function setKeysForSaveQuery(Builder $query)
{
    $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());

    return $query;
}

所以實際上,Model 呼叫的也是 builderdelete 函式。

軟刪除

如果想要使用軟刪除,需要使用 Illuminate\Database\Eloquent\SoftDeletes 這個 trait。並且需要定義軟刪除欄位,預設為 deleted_at,將軟刪除欄位放入 dates 中,具體用法可參考官方文件:軟刪除

class Flight extends Model
{
    use SoftDeletes;

    /**
     * 需要被轉換成日期的屬性。
     *
     * @var array
     */
    protected $dates = ['deleted_at'];
}

我們先看看這個 trait

trait SoftDeletes 
{
    public static function bootSoftDeletes()
    {
        static::addGlobalScope(new SoftDeletingScope);
    }

}

如果使用了軟刪除,在 model 的啟動過程中,就會啟動軟刪除的這個函式。可以看出來,軟刪除是需要查詢作用域來合作發揮作用的。我們看看這個 SoftDeletingScope :

class SoftDeletingScope implements Scope
{
    protected $extensions = ['Restore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed'];

    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull($model->getQualifiedDeletedAtColumn());
    }

    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);

            return $builder->update([
                $column => $builder->getModel()->freshTimestampString(),
            ]);
        });
    }
}

apply 函式是載入全域性域呼叫的函式,每次進行查詢的時候,呼叫 get 函式就會自動載入這個函式,whereNull 這個查詢條件會被載入到具體的 where 條件中。deleted_at 欄位一般被設定為 null,在執行軟刪除的時候,該欄位會被賦予時間格式的值,標誌著被刪除的時間。

在載入全域性作用域的時候,還會呼叫 extend 函式,extend 函式為 model 新增了四個函式:

  • WithTrashed
 protected function addWithTrashed(Builder $builder)
{
    $builder->macro('withTrashed', function (Builder $builder) {
        return $builder->withoutGlobalScope($this);
    });
}

withTrashed 函式取消了軟刪除的全域性作用域,這樣我們查詢資料的時候就會查詢到正常資料和被軟刪除的資料。

  • withoutTrashed
protected function addWithoutTrashed(Builder $builder)
{
    $builder->macro('withoutTrashed', function (Builder $builder) {
        $model = $builder->getModel();

        $builder->withoutGlobalScope($this)->whereNull(
            $model->getQualifiedDeletedAtColumn()
        );

        return $builder;
    });
}

withTrashed 函式著重強調了不要獲取軟刪除的資料。

  • onlyTrashed
protected function addOnlyTrashed(Builder $builder)
{
    $builder->macro('onlyTrashed', function (Builder $builder) {
        $model = $builder->getModel();

        $builder->withoutGlobalScope($this)->whereNotNull(
            $model->getQualifiedDeletedAtColumn()
        );

        return $builder;
    });
}

如果只想獲取被軟刪除的資料,可以使用這個函式 onlyTrashed,可以看到,它使用了 whereNotNull

  • restore
protected function addRestore(Builder $builder)
{
    $builder->macro('restore', function (Builder $builder) {
        $builder->withTrashed();

        return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]);
    });
}

如果想要恢復被刪除的資料,還可以使用 restore,重新將 deleted_at 資料恢復為 null。

performDeleteOnModel

SoftDeletes 這個 trait 會過載 performDeleteOnModel 函式,它將不會呼叫 Eloquent Builderdelete 方法,而是採用更新操作:

protected function performDeleteOnModel()
{
    if ($this->forceDeleting) {
        return $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey())->forceDelete();
    }

    return $this->runSoftDelete();
}

protected function runSoftDelete()
{
    $query = $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey());

    $time = $this->freshTimestamp();

    $columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)];

    $this->{$this->getDeletedAtColumn()} = $time;

    if ($this->timestamps) {
        $this->{$this->getUpdatedAtColumn()} = $time;

        $columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
    }

    $query->update($columns);
}

刪除操作不僅更新了 deleted_at,還更新了 updated_at 欄位。

相關文章