Изучение основного кода Laravel — добавление, удаление, модификация модели и проверка базовой реализации.

база данных PHP SQL Laravel

В прошлой статье мы говорили о построителе запросов к базе данных, Query Builder, и узнали о реализации кода Fluent Api, предоставляемого Query Builder, для построения и генерации операторов SQL. В этой статье мы изучим еще одну важную часть базы данных Laravel: Eloquent Model.

Eloquent Model абстрагирует атрибуты и ассоциации таблицы данных в каждый класс Model, поэтому класс Model является абстракцией таблицы данных, а объект Model — абстракцией одной записи в таблице. Основанный на упомянутом выше Query Builder, Eloquent Model предоставляет Eloquent Builder для взаимодействия с базой данных, а также обеспечивает ассоциацию моделей для элегантного решения отношений между несколькими таблицами данных.

Загрузить Eloquent Builder

Eloquent Builder реализован на базе упомянутого выше Query Builder, рассмотрим его на конкретных примерах.

DB::table('user')->where('name', 'James')->where('age', 27)->get();

После перезаписи его для использования модели он становится

User::where('name', 'James')->where('age', 27)->get();

В файле класса Model мы не нашлиwhere,find,firstЭти часто используемые методы запросов, мы все знаем, что PHP будет запускать магические методы при вызове несуществующего метода класса.__callStatic, вызов несуществующего метода экземпляра вызовет__call, Нетрудно догадаться, что вышеуказанные методы вызываются динамически через эти два магических метода.Давайте посмотрим на исходный код.

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

}

Запрос модели

Из приведенных выше кодов мы можем видеть, что методы, связанные с запросами, вызываемые в модели, в конечном итоге будут проходить__callВместо этого вызовите эти методы экземпляра Eloquent Builder. Взаимодействие между Eloquent Builder и базовой базой данных достигается за счет использования Query Builder. Мы видим, что при создании экземпляра Eloquent Builder объект QueryBuilder соединения с базой данных передается методу его конструктора. , давайте взглянем на исходный код 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;
}

Метод where Eloquent Builder в коде напрямую передает запрос в Query Builder после получения запроса на вызовwhereметод, а затем метод get также сначала пропускается через Query BuildergetМетод выполняет запрос для получения массива результатов, а затем передаетnewFromBuilderЭтот метод преобразует результирующий массив в коллекцию объектов Model и другой, более часто используемый метод.firstТакже вgetРеализован на основе метода применить ограничение 1 к запросу, а затем начать сgetКоллекция, возвращаемая методом, используетсяArr::first()Выньте объект модели и верните его вызывающей стороне.

Обновление модели

Прочитав реализацию запроса модели, давайте взглянем на реализацию обновления, создания и удаления или продолжим расширение примера запроса в начале:

$user = User::where('name', 'James')->where('age', 27)->first();

Теперь через запрос модели мы получаем экземпляр модели пользователя, теперь мы хотим изменить возраст этого пользователя на 28 лет:

$user->age = 28;
$user->save();

Мы знаем, что атрибуты модели соответствуют полям таблицы данных.Когда приведенный выше метод get возвращает коллекцию экземпляров модели, мы видели, что поля и значения полей записей данных назначаются атрибутам $ атрибут экземпляра модели, и экземпляр модели доступен и установлен.Свойства, соответствующие этим полям, передаются через__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;
    }	
}

Если Модель определяет модификатор свойства, модификатор будет выполняться при установке свойства В нашем примере модификатор свойства не используется. при исполнении$user->age = 28, свойство $attributes в экземпляре модели пользователя станет

protected $attributes = [
	...
	'age' => 28,
	...
]

После установки нового значения атрибута выполнение метода сохранения модели Eloquent обновит соответствующую запись в базе данных.Давайте посмотрим на логику в методе сохранения:

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

При сохранении он будет основан на экземпляре модели.existsатрибут, чтобы определить, выполнять ли обновление или вставку, пример, который мы используем здесь, это обновление, и программа выполняет сравнение во время обновления$attributesа также$originalЗначение поля каждого поля в двух атрибутах массива находит измененное поле (при получении объекта Model поле таблицы данных будет сохранено в$attributesа также$originalдва атрибута), если нет измененных полей, то на этом обновление заканчивается, а если есть изменения, то продолжаем выполнениеperformUpdateметод,performUpdateМетод будет выполнять метод обновления Eloquent Builder, а Eloquent Builder зависит от экземпляра Query Builder подключения к базе данных для последнего выполненного обновления базы данных.

МодельWrite

Я просто сказал, что когда модель получена через Eloquent Model (вnewFromBuilderметод) преобразует экземпляр моделиexistsДля этого свойства установлено значение true, тогда значение этого свойства для вновь созданного экземпляра модели равно 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Если таблица автоматически инкрементируется по первичному ключу, то после вставки значение идентификатора первичного ключа новой записи будет установлено в атрибут экземпляра Модели, а также это поможет нам сохранить поле времени иexistsАтрибуты.

Удаление модели

Операция удаления модели Eloquent такая же.Метод удаления в построителе запросов подключения к базе данных выполняется через Eloquent Builder для удаления записи базы данных:

//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 в вызов метода передается соответствующему методу в построителе запросов для завершения операции, поэтому методы, которые можно использовать в построителе запросов, также можно использовать в модели Eloquent.

Помимо абстракции таблиц данных и базового CRUD, еще одной важной особенностью модели является ассоциация моделей, которая помогает нам элегантно решать отношения между таблицами данных. Мы подробно рассмотрим реализацию части ассоциации модели в следующей статье.

Эта статья вошла в серию статейИзучение основного кода Laravel, добро пожаловать в гости и читать.