当前位置: 首页 > 文档资料 > Laravel 源码详解 >

Laravel Database 数据库 - Laravel Database——Eloquent Model 源码分析(下)

优质
小牛编辑
125浏览
2023-12-01

获取模型

get 函数

  1. public function get($columns = ['*'])
  2. {
  3. $builder = $this->applyScopes();
  4. if (count($models = $builder->getModels($columns)) > 0) {
  5. $models = $builder->eagerLoadRelations($models);
  6. }
  7. return $builder->getModel()->newCollection($models);
  8. }
  9. public function getModels($columns = ['*'])
  10. {
  11. return $this->model->hydrate(
  12. $this->query->get($columns)->all()
  13. )->all();
  14. }

get 函数会将 QueryBuilder 所获取的数据进一步包装 hydratehydrate 函数会将数据库取回来的数据打包成数据库模型对象 Eloquent Model,如果可以获取到数据,还会利用函数 eagerLoadRelations 来预加载关系模型。

  1. public function hydrate(array $items)
  2. {
  3. $instance = $this->newModelInstance();
  4. return $instance->newCollection(array_map(function ($item) use ($instance) {
  5. return $instance->newFromBuilder($item);
  6. }, $items));
  7. }

newModelInstance 函数创建了一个新的数据库模型对象,重要的是这个函数为新的数据库模型对象赋予了 connection

  1. public function newModelInstance($attributes = [])
  2. {
  3. return $this->model->newInstance($attributes)->setConnection(
  4. $this->query->getConnection()->getName()
  5. );
  6. }

newFromBuilder 函数会将所有数据库数据存入另一个新的 Eloquent Modelattributes 中:

  1. public function newFromBuilder($attributes = [], $connection = null)
  2. {
  3. $model = $this->newInstance([], true);
  4. $model->setRawAttributes((array) $attributes, true);
  5. $model->setConnection($connection ?: $this->getConnectionName());
  6. $model->fireModelEvent('retrieved', false);
  7. return $model;
  8. }

newInstance 函数专用于创建新的数据库对象模型:

  1. public function newInstance($attributes = [], $exists = false)
  2. {
  3. $model = new static((array) $attributes);
  4. $model->exists = $exists;
  5. $model->setConnection(
  6. $this->getConnectionName()
  7. );
  8. return $model;
  9. }

值得注意的是 newInstanceexist 设置为 true,意味着当前这个数据库模型对象是从数据库中获取而来,并非是手动新建的,这个 exist 为真,我们才能对这个数据库对象进行 update

setRawAttributes 函数为新的数据库对象赋予属性值,并且进行 sync,标志着对象的原始状态:

  1. public function setRawAttributes(array $attributes, $sync = false)
  2. {
  3. $this->attributes = $attributes;
  4. if ($sync) {
  5. $this->syncOriginal();
  6. }
  7. return $this;
  8. }
  9. public function syncOriginal()
  10. {
  11. $this->original = $this->attributes;
  12. return $this;
  13. }

这个原始状态的记录十分重要,原因是 save 函数就是利用原始值 original 与属性值 attributes 的差异来决定更新的字段。

find 函数

find 函数用于利用主键 id 来查询数据,find 函数也可以传入数组,查询多个数据

  1. public function find($id, $columns = ['*'])
  2. {
  3. if (is_array($id) || $id instanceof Arrayable) {
  4. return $this->findMany($id, $columns);
  5. }
  6. return $this->whereKey($id)->first($columns);
  7. }
  8. public function findMany($ids, $columns = ['*'])
  9. {
  10. if (empty($ids)) {
  11. return $this->model->newCollection();
  12. }
  13. return $this->whereKey($ids)->get($columns);
  14. }

findOrFail

laravel 还提供 findOrFail 函数,一般用于 controller,在未找到记录的时候会抛出异常。

  1. public function findOrFail($id, $columns = ['*'])
  2. {
  3. $result = $this->find($id, $columns);
  4. if (is_array($id)) {
  5. if (count($result) == count(array_unique($id))) {
  6. return $result;
  7. }
  8. } elseif (! is_null($result)) {
  9. return $result;
  10. }
  11. throw (new ModelNotFoundException)->setModel(
  12. get_class($this->model), $id
  13. );
  14. }

其他查询与数据获取方法

所用 Query Builder 支持的查询方法,例如 selectselectSubwhereDatewhereBetween 等等,都可以直接对 Eloquent Model 直接使用,程序会通过魔术方法调用 Query Builder 的相关方法:

  1. protected $passthru = [
  2. 'insert', 'insertGetId', 'getBindings', 'toSql',
  3. 'exists', 'count', 'min', 'max', 'avg', 'sum', 'getConnection',
  4. ];
  5. public function __call($method, $parameters)
  6. {
  7. ...
  8. if (in_array($method, $this->passthru)) {
  9. return $this->toBase()->{$method}(...$parameters);
  10. }
  11. $this->query->{$method}(...$parameters);
  12. return $this;
  13. }

passthru 中的各个函数在调用前需要加载查询作用域,原因是这些操作基本上是 aggregate 的,需要添加搜索条件才能更加符合预期:

  1. public function toBase()
  2. {
  3. return $this->applyScopes()->getQuery();
  4. }

添加和更新模型

save 函数

Eloquent Model 中,添加与更新模型可以统一用 save 函数。在添加模型的时候需要事先为 model 属性赋值,可以单个手动赋值,也可以批量赋值。在更新模型的时候,需要事先从数据库中取出模型,然后修改模型属性,最后执行 save 更新操作。官方文档:添加和更新模型

  1. public function save(array $options = [])
  2. {
  3. $query = $this->newQueryWithoutScopes();
  4. if ($this->fireModelEvent('saving') === false) {
  5. return false;
  6. }
  7. if ($this->exists) {
  8. $saved = $this->isDirty() ?
  9. $this->performUpdate($query) : true;
  10. }
  11. else {
  12. $saved = $this->performInsert($query);
  13. if (! $this->getConnectionName() &&
  14. $connection = $query->getConnection()) {
  15. $this->setConnection($connection->getName());
  16. }
  17. }
  18. if ($saved) {
  19. $this->finishSave($options);
  20. }
  21. return $saved;
  22. }

save 函数不会加载全局作用域,原因是凡是利用 save 函数进行的插入或者更新的操作都不会存在 where 条件,仅仅利用自身的主键属性来进行更新。如果需要 where 条件可以使用 query\builderupdate 函数,我们在下面会详细介绍:

  1. public function newQueryWithoutScopes()
  2. {
  3. $builder = $this->newEloquentBuilder($this->newBaseQueryBuilder());
  4. return $builder->setModel($this)
  5. ->with($this->with)
  6. ->withCount($this->withCount);
  7. }
  8. protected function newBaseQueryBuilder()
  9. {
  10. $connection = $this->getConnection();
  11. return new QueryBuilder(
  12. $connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
  13. );
  14. }

newQueryWithoutScopes 函数创建新的没有任何其他条件的 Eloquent\builder 类,而 Eloquent\builder 类需要 Query\builder 类作为底层查询构造器。

performUpdate 函数

如果当前的数据库模型对象是从数据库中取出的,也就是直接或间接的调用 get() 函数从数据库中获取到的数据库对象,那么其 exists 必然是 true

  1. public function isDirty($attributes = null)
  2. {
  3. return $this->hasChanges(
  4. $this->getDirty(), is_array($attributes) ? $attributes : func_get_args()
  5. );
  6. }
  7. public function getDirty()
  8. {
  9. $dirty = [];
  10. foreach ($this->getAttributes() as $key => $value) {
  11. if (! $this->originalIsEquivalent($key, $value)) {
  12. $dirty[$key] = $value;
  13. }
  14. }
  15. return $dirty;
  16. }

getDirty 函数可以获取所有与原始值不同的属性值,也就是需要更新的数据库字段。关键函数在于 originalIsEquivalent

  1. protected function originalIsEquivalent($key, $current)
  2. {
  3. if (! array_key_exists($key, $this->original)) {
  4. return false;
  5. }
  6. $original = $this->getOriginal($key);
  7. if ($current === $original) {
  8. return true;
  9. } elseif (is_null($current)) {
  10. return false;
  11. } elseif ($this->isDateAttribute($key)) {
  12. return $this->fromDateTime($current) ===
  13. $this->fromDateTime($original);
  14. } elseif ($this->hasCast($key)) {
  15. return $this->castAttribute($key, $current) ===
  16. $this->castAttribute($key, $original);
  17. }
  18. return is_numeric($current) && is_numeric($original)
  19. && strcmp((string) $current, (string) $original) === 0;
  20. }

可以看到,对于数据库可以转化的属性都要先进行转化,然后再开始对比。比较出的结果,就是我们需要 update 的字段。

执行更新的时候,除了 getDirty 函数获得的待更新字段,还会有 UPDATED_AT 这个字段:

  1. protected function performUpdate(Builder $query)
  2. {
  3. if ($this->fireModelEvent('updating') === false) {
  4. return false;
  5. }
  6. if ($this->usesTimestamps()) {
  7. $this->updateTimestamps();
  8. }
  9. $dirty = $this->getDirty();
  10. if (count($dirty) > 0) {
  11. $this->setKeysForSaveQuery($query)->update($dirty);
  12. $this->fireModelEvent('updated', false);
  13. $this->syncChanges();
  14. }
  15. return true;
  16. }
  17. protected function updateTimestamps()
  18. {
  19. $time = $this->freshTimestamp();
  20. if (! is_null(static::UPDATED_AT) && ! $this->isDirty(static::UPDATED_AT)) {
  21. $this->setUpdatedAt($time);
  22. }
  23. if (! $this->exists && ! $this->isDirty(static::CREATED_AT)) {
  24. $this->setCreatedAt($time);
  25. }
  26. }

执行更新的时候,where 条件只有一个,那就是主键 id

  1. protected function setKeysForSaveQuery(Builder $query)
  2. {
  3. $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());
  4. return $query;
  5. }
  6. protected function getKeyForSaveQuery()
  7. {
  8. return $this->original[$this->getKeyName()]
  9. ?? $this->getKey();
  10. }
  11. public function getKey()
  12. {
  13. return $this->getAttribute($this->getKeyName());
  14. }

最后会调用 EloquentBuilderupdate 函数:

  1. public function update(array $values)
  2. {
  3. return $this->toBase()->update($this->addUpdatedAtColumn($values));
  4. }
  5. protected function addUpdatedAtColumn(array $values)
  6. {
  7. if (! $this->model->usesTimestamps()) {
  8. return $values;
  9. }
  10. return Arr::add(
  11. $values, $this->model->getUpdatedAtColumn(),
  12. $this->model->freshTimestampString()
  13. );
  14. }
  15. public function freshTimestampString()
  16. {
  17. return $this->fromDateTime($this->freshTimestamp());
  18. }
  19. public function fromDateTime($value)
  20. {
  21. return is_null($value) ? $value : $this->asDateTime($value)->format(
  22. $this->getDateFormat()
  23. );
  24. }

performInsert

关于数据库对象的插入,如果数据库的主键被设置为 increment,也就是自增的话,程序会调用 insertAndSetId,这个时候不需要给数据库模型对象手动赋值主键 id。若果数据库的主键并不支持自增,那么就需要在插入前,为数据库对象的主键 id 赋值,否则数据库会报错。

  1. protected function performInsert(Builder $query)
  2. {
  3. if ($this->fireModelEvent('creating') === false) {
  4. return false;
  5. }
  6. if ($this->usesTimestamps()) {
  7. $this->updateTimestamps();
  8. }
  9. $attributes = $this->attributes;
  10. if ($this->getIncrementing()) {
  11. $this->insertAndSetId($query, $attributes);
  12. }
  13. else {
  14. if (empty($attributes)) {
  15. return true;
  16. }
  17. $query->insert($attributes);
  18. }
  19. $this->exists = true;
  20. $this->wasRecentlyCreated = true;
  21. $this->fireModelEvent('created', false);
  22. return true;
  23. }

laravel 默认数据库的主键支持自增属性,程序调用的也是函数 insertAndSetId 函数:

  1. protected function insertAndSetId(Builder $query, $attributes)
  2. {
  3. $id = $query->insertGetId($attributes, $keyName = $this->getKeyName());
  4. $this->setAttribute($keyName, $id);
  5. }

插入后,会将插入后得到的主键 id 返回,并赋值到模型的属性当中。

如果数据库主键不支持自增,那么我们在数据库类中要设置:

  1. public $incrementing = false;

每次进行插入数据的时候,需要手动给主键赋值。

update 函数

save 函数仅仅支持手动的属性赋值,无法批量赋值。laravelEloquent Model 还有一个函数: update 支持批量属性赋值。有意思的是,Eloquent Builder 也有函数 update,那个是上一小节提到的 performUpdate 所调用的函数。

两个 update 功能一致,只是 Modelupdate 函数比较适用于更新从数据库取回的数据库对象:

  1. $flight = App\Flight::find(1);
  2. $flight->update(['name' => 'New Flight Name','desc' => 'test']);

Builderupdate 适用于多查询条件下的更新:

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

无论哪一种,都会自动更新 updated_at 字段。

Modelupdate 函数借助 fill 函数与 save 函数:

  1. public function update(array $attributes = [], array $options = [])
  2. {
  3. if (! $this->exists) {
  4. return false;
  5. }
  6. return $this->fill($attributes)->save($options);
  7. }

make 函数

同样的,save 的插入也仅仅支持手动属性赋值,如果想实现批量属性赋值的插入可以使用 make 函数:

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

make 函数实际上仅仅是新建了一个 Eloquent Model,并批量赋予属性值:

  1. public function make(array $attributes = [])
  2. {
  3. return $this->newModelInstance($attributes);
  4. }
  5. public function newModelInstance($attributes = [])
  6. {
  7. return $this->model->newInstance($attributes)->setConnection(
  8. $this->query->getConnection()->getName()
  9. );
  10. }

create 函数

如果想要一步到位,批量赋值属性与插入一起操作,可以使用 create 函数:

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

相比较 make 函数,create 函数更进一步调用了 save 函数:

  1. public function create(array $attributes = [])
  2. {
  3. return tap($this->newModelInstance($attributes), function ($instance) {
  4. $instance->save();
  5. });
  6. }

实际上,属性值是否可以批量赋值需要受 fillableguarded 来控制,如果我们想要强制批量赋值可以使用 forceCreate

  1. public function forceCreate(array $attributes)
  2. {
  3. return $this->model->unguarded(function () use ($attributes) {
  4. return $this->newModelInstance()->create($attributes);
  5. });
  6. }

findOrNew 函数

laravel 提供一种主键查询或者新建数据库对象的函数:findOrNew

  1. public function findOrNew($id, $columns = ['*'])
  2. {
  3. if (! is_null($model = $this->find($id, $columns))) {
  4. return $model;
  5. }
  6. return $this->newModelInstance();
  7. }

值得注意的是,当查询失败的时候,会返回一个全新的数据库对象,不含有任何 attributes

firstOrNew 函数

laravel 提供一种自定义查询或者新建数据库对象的函数:firstOrNew

  1. public function firstOrNew(array $attributes, array $values = [])
  2. {
  3. if (! is_null($instance = $this->where($attributes)->first())) {
  4. return $instance;
  5. }
  6. return $this->newModelInstance($attributes + $values);
  7. }

值得注意的是,如果查询失败,会返回一个含有 attributesvalues 两者合并的属性的数据库对象。

firstOrCreate 函数

类似于 firstOrNew 函数,firstOrCreate 函数也用于自定义查询或者新建数据库对象,不同的是,firstOrCreate 函数还进一步对数据进行了插入操作:

  1. public function firstOrCreate(array $attributes, array $values = [])
  2. {
  3. if (! is_null($instance = $this->where($attributes)->first())) {
  4. return $instance;
  5. }
  6. return tap($this->newModelInstance($attributes + $values), function ($instance) {
  7. $instance->save();
  8. });
  9. }

updateOrCreate 函数

firstOrCreate 函数基础上,除了对数据进行查询,还会对查询成功的数据利用 value 进行更新:

  1. public function updateOrCreate(array $attributes, array $values = [])
  2. {
  3. return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
  4. $instance->fill($values)->save();
  5. });
  6. }

firstOr 函数

如果想要自定义查找失败后的操作,可以使用 firstOr 函数,该函数可以传入闭包函数,处理找不到数据的情况:

  1. public function firstOr($columns = ['*'], Closure $callback = null)
  2. {
  3. if ($columns instanceof Closure) {
  4. $callback = $columns;
  5. $columns = ['*'];
  6. }
  7. if (! is_null($model = $this->first($columns))) {
  8. return $model;
  9. }
  10. return call_user_func($callback);
  11. }

删除模型

删除模型也会分为两种,一种是针对 Eloquent Model 的删除,这种删除必须是从数据库中取出的对象。还有一种是 Eloquent Builder 的删除,这种删除一般会带有多个查询条件。我们这一小节主要讲 model 的删除:

  1. public function delete()
  2. {
  3. if (is_null($this->getKeyName())) {
  4. throw new Exception('No primary key defined on model.');
  5. }
  6. if (! $this->exists) {
  7. return;
  8. }
  9. if ($this->fireModelEvent('deleting') === false) {
  10. return false;
  11. }
  12. $this->touchOwners();
  13. $this->performDeleteOnModel();
  14. $this->fireModelEvent('deleted', false);
  15. return true;
  16. }

删除模型时,模型对象必然要有主键。performDeleteOnModel 函数执行具体的删除操作:

  1. protected function performDeleteOnModel()
  2. {
  3. $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete();
  4. $this->exists = false;
  5. }
  6. protected function setKeysForSaveQuery(Builder $query)
  7. {
  8. $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());
  9. return $query;
  10. }

所以实际上,Model 调用的也是 builderdelete 函数。

软删除

如果想要使用软删除,需要使用 Illuminate\Database\Eloquent\SoftDeletes 这个 trait。并且需要定义软删除字段,默认为 deleted_at,将软删除字段放入 dates 中,具体用法可参考官方文档:软删除

  1. class Flight extends Model
  2. {
  3. use SoftDeletes;
  4. /**
  5. * 需要被转换成日期的属性。
  6. *
  7. * @var array
  8. */
  9. protected $dates = ['deleted_at'];
  10. }

我们先看看这个 trait

  1. trait SoftDeletes
  2. {
  3. public static function bootSoftDeletes()
  4. {
  5. static::addGlobalScope(new SoftDeletingScope);
  6. }
  7. }

如果使用了软删除,在 model 的启动过程中,就会启动软删除的这个函数。可以看出来,软删除是需要查询作用域来合作发挥作用的。我们看看这个 SoftDeletingScope :

  1. class SoftDeletingScope implements Scope
  2. {
  3. protected $extensions = ['Restore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed'];
  4. public function apply(Builder $builder, Model $model)
  5. {
  6. $builder->whereNull($model->getQualifiedDeletedAtColumn());
  7. }
  8. public function extend(Builder $builder)
  9. {
  10. foreach ($this->extensions as $extension) {
  11. $this->{"add{$extension}"}($builder);
  12. }
  13. $builder->onDelete(function (Builder $builder) {
  14. $column = $this->getDeletedAtColumn($builder);
  15. return $builder->update([
  16. $column => $builder->getModel()->freshTimestampString(),
  17. ]);
  18. });
  19. }
  20. }

apply 函数是加载全局域调用的函数,每次进行查询的时候,调用 get 函数就会自动加载这个函数,whereNull 这个查询条件会被加载到具体的 where 条件中。deleted_at 字段一般被设置为 null,在执行软删除的时候,该字段会被赋予时间格式的值,标志着被删除的时间。

在加载全局作用域的时候,还会调用 extend 函数,extend 函数为 model 添加了四个函数:

  • WithTrashed
  1. protected function addWithTrashed(Builder $builder)
  2. {
  3. $builder->macro('withTrashed', function (Builder $builder) {
  4. return $builder->withoutGlobalScope($this);
  5. });
  6. }

withTrashed 函数取消了软删除的全局作用域,这样我们查询数据的时候就会查询到正常数据和被软删除的数据。

  • withoutTrashed
  1. protected function addWithoutTrashed(Builder $builder)
  2. {
  3. $builder->macro('withoutTrashed', function (Builder $builder) {
  4. $model = $builder->getModel();
  5. $builder->withoutGlobalScope($this)->whereNull(
  6. $model->getQualifiedDeletedAtColumn()
  7. );
  8. return $builder;
  9. });
  10. }

withTrashed 函数着重强调了不要获取软删除的数据。

  • onlyTrashed
  1. protected function addOnlyTrashed(Builder $builder)
  2. {
  3. $builder->macro('onlyTrashed', function (Builder $builder) {
  4. $model = $builder->getModel();
  5. $builder->withoutGlobalScope($this)->whereNotNull(
  6. $model->getQualifiedDeletedAtColumn()
  7. );
  8. return $builder;
  9. });
  10. }

如果只想获取被软删除的数据,可以使用这个函数 onlyTrashed,可以看到,它使用了 whereNotNull

  • restore
  1. protected function addRestore(Builder $builder)
  2. {
  3. $builder->macro('restore', function (Builder $builder) {
  4. $builder->withTrashed();
  5. return $builder->update([$builder->getModel()->getDeletedAtColumn() => null]);
  6. });
  7. }

如果想要恢复被删除的数据,还可以使用 restore,重新将 deleted_at 数据恢复为 null。

performDeleteOnModel

SoftDeletes 这个 trait 会重载 performDeleteOnModel 函数,它将不会调用 Eloquent Builderdelete 方法,而是采用更新操作:

  1. protected function performDeleteOnModel()
  2. {
  3. if ($this->forceDeleting) {
  4. return $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey())->forceDelete();
  5. }
  6. return $this->runSoftDelete();
  7. }
  8. protected function runSoftDelete()
  9. {
  10. $query = $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey());
  11. $time = $this->freshTimestamp();
  12. $columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)];
  13. $this->{$this->getDeletedAtColumn()} = $time;
  14. if ($this->timestamps) {
  15. $this->{$this->getUpdatedAtColumn()} = $time;
  16. $columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
  17. }
  18. $query->update($columns);
  19. }

删除操作不仅更新了 deleted_at,还更新了 updated_at 字段。