【Laravel系列4.3】模型Eloquent ORM的使用(一)

2023-03-03 13:14:45 浏览数 (2)

模型Eloquent ORM的使用(一)

先来说说 ORM 是什么,不知道有没有不清楚这个概念的小伙伴,反正这里就一道科普一下算了。ORM 的全称是 Obejct Relational Mapping ,翻译过来就是 对象关系映射 ,再说得直白一点,就是用 面向对象 里的对象来 映射 数据库中的数据。我们在关系型数据库中,一行数据就可以看成是一个对象,整个表就可以看成是这个对象的列表。这就是非常简单地针对 ORM 的理解。

Java 中的 Hibernate 就是早期非常经典的 ORM 框架。而在 Yii 中使用的是 Active Record 这种类型的领域模型模式,在 Yii 中甚至这个组件的名称就直接是 AR 。Active Record 中文的意思是活动记录,特点是一个模型类对应数据库中的一个表。其实,Laravel 中的 Eloquent ORM 也是 Active Record 的实现,这也是现在 ORM 的主流。

通过前两篇文章的铺垫,我们很容易就能操作 Laravel 中的模型,但是,真正要改变的是你看待这种操作数据库的方式。要把数据库里的数据想像成是编程语言中的对象,这才是 ORM 最主要的内容。

创建一个模型

创建模型我们可以手动,也可以通过命令行,既然是学习框架,那么我们还是通过命令行来创建一个模型类吧。使用的表依然是之前的表,不过还是改下名字吧,这回表名就叫做 m_test 。然后,我们就通过命令行创建这个表对应的 模型 类。

代码语言:javascript复制
php artisan make:model MTest

执行命令之后,我们会在 app/Models 目录下看到新创建的 MTest.php 文件,生成的代码是这样的:

代码语言:javascript复制
namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class MTest extends Model
{
    use HasFactory;
}

嗯,就这么简单,一个模型类就创建成功了。接下来我们就来使用它进行增删改查的操作。

增删改查

首先,我们先来看一个新增的例子。

代码语言:javascript复制
Route::get('model/test/insert', function () {
    $data = [
        [
            'name'=>'Peter',
            'sex' => 1,
        ],
        [
            'name'=>'Tom',
            'sex' => 1,
        ],
        [
            'name'=>'Susan',
            'sex' => 2,
        ],
        [
            'name'=>'Mary',
            'sex' => 2,
        ],
        [
            'name'=>'Jim',
            'sex' => 1,
        ],
    ];
    foreach ($data as $v) {
        $model = new AppModelsMTest();
        $model->name = $v['name'];
        $model->sex = $v['sex'];
        $model->insertGetId();
        $insertId = $model->id;
        // $insertId = AppModelsMTest::insertGetId($v);
        echo $insertId, '<br/>';
    }
});

// Base table or view not found: 1146 Table 'laravel.m_tests' doesn't exist

直接执行这段代码,报错了!!这是为啥?赶紧查看错误信息,竟然是这个 laravel.m_tests 表不存在。小伙伴们不要惊讶,在这里出错是正常的,为什么呢?一是在上面的 Modal 类中,我们没有指定表名,但是框架会根据类名映射一个表名出来。规则是将大驼峰变成蛇式命名,比如 MTest 会变成 m_test 。这样看貌似没问题呀,可是为什么报错的是 m_tests 表不存在呢?这就牵涉到上面 Active Record 的概念了,在 AR 中,一个类对应的是一张表,而一张表是由多行数据组成的。在英文命名中,复数一般都会加 s 的,所以,如果是走的自动映射表名的话,会在大驼峰转换之后再加一个 s 到表名后面。

好吧,原来如此,但是这样我们就用不了这个表了?不不不,非常简单,我们给 Model 类设置一个变量用于指定表名就可以了。

代码语言:javascript复制
class MTest extends Model
{
    use HasFactory;
    protected $table = 'm_test';
}

再次运行上面的插入,看看是不是插入成功了?我去,还是报错,我们再看下错误信息。

代码语言:javascript复制
// Unknown column 'updated_at' in 'field list' 

这又是什么鬼?我们的表里没有这个字段呀。

其实,这也是默认 Model 的一种机制。对于 Laravel 中标准的 Eloquent 模型类来说,每个表都应该有两个字段,一个是 updated_at ,另一个是 created_at ,分别是两个时间戳字段,用于记录数据的创建时间和修改时间。其实所有的表最好都有这两个字段,而且很多后台管理系统中还需要有 创建人 和 修改人 的记录。它们的目的都是为了数据的安全和记录可追溯。如果你的表中有这两个字段的话,那么在 Model 操作的过程中,你可以忽略这两个字段的操作,Model 系统会自动设置它们。但是在我们今天的演示中,不需要这两个字段,所以也可以设置一个属性来关闭 Model 针对它们的自动处理。

代码语言:javascript复制
class MTest extends Model
{
    use HasFactory;
    protected $table = 'm_test';
    public $timestamps = false;
}

好吧,再次尝试一下。总算是运行成功了吧,我们再把修改、删除和简单的查询的代码都放出来,后面再一起看看它们是怎么运行的。

代码语言:javascript复制
Route::get('model/test/update', function () {
    $data = [
        'name' => request()->name,
        'sex' => request()->sex,
        'id' => request()->id
    ];

    if($data['id'] < 1 || !$data['name'] || !in_array($data['sex'], [1, 2])){
        echo '参数错误';
    }

    $model = AppModelsMTest::find($data['id']);
    $model->name = $data['name'];
    $model->sex = $data['sex'];
    $model->update();

    echo '修改成功';
});

Route::get('model/test/delete', function () {

    $id = request()->id;
    if($id < 1){
        echo '参数错误';
    }

    AppModelsMTest::destroy($id);

    echo '删除成功';
});

Route::get('model/test/list', function () {
    $where = [];
    if(request()->name){
        $where[] = ['name', 'like', '%' . request()->name . '%'];
    }
    if(request()->sex){
        $where[] = ['sex', '=', request()->sex];
    }

    $list = AppModelsMTest::where($where)
        ->orderBy('id', 'desc')
        ->limit(10)
        ->offset(0)
        ->get()
        ->toArray();

    dd($list);
});

Route::get('model/test/info', function () {
    $id = (int)request()->get('id', 0);

    $info = AppModelsMTest::find($id);
    
    dd($info);
});

先来看看插入功能,也就是最上面的代码中的功能。我们实例化一个 MTest 对象,然后给它的属性赋值,之后直接 save() 就可以了。在这里比较奇怪的是,我们在实例化和赋值的过程中没有给对象的主键 id 赋值,但是在 save() 之后,id 就有值了,而且是我们新插入的数据 id ,是不是很高大上。没错,这就是 ORM 的优势,其实我们的这个实例对象已经和数据库里的那一条数据绑定上了。注意看代码中注释的部分,我们用 MTest::insertGetId() 这种形式也是可以插入成功的,只是这种形式是更类似于 查询构造器 的方式了,不太能体现出 ORM 的感觉,所以还是使用实例化对象的方式来操作。

同样,更新的时候我们是先通过静态方法 find() 查找并返回一个数据对象,然后修改它的属性再 update() 就可以了。注意,这里也可以使用 save() 方法的,它的作用是即可以用于新增也可以用于保存,在 查询构造器 中没有这个方法,但是有一个类似的 updateOrInsert() 方法,大家可以自己试试。

删除功能直接调用的是静态的 destroy() 方法,它可以接收的参数是主键 id ,而且这个地方我们可以传递多个 id 以及其它不同的写法就能够实现批量删除,大家也可以自行查阅官方文档。

最后在查询中,我们也看到了类似于 查询构造器 的链式调用形式,通过模型的静态 where() 方法返回的实例对象,一步步地构造整个查询。这个原理我相信已经不用我多解释了,和 查询构造器 的不同就是这里是通过 Model 起步开始构造的,而不是直接通过 DatabaseManager 起步的。但其实在 Model 的底层,肯定也是有一个 DatabaseManager 和对应的 Connector 在起作用。这个我们后面分析源码的时候再说。

看到这里,是不是感觉前两篇文章的内容非常重要呀,如果还没搞明白的同学请马上回去再看看前两篇文章的内容,学习就是这样循序渐进,如果一上来就讲 Model 层的这堆东西,估计谁都会发懵。接下来还是几个小操作的演示,源码的分析我们依然放到最后。

关联操作

关联操作是什么意思呢?这个其实和数据库的关联操作是有关系的。在标准的数据库结构中,我们是有主外键的概念的,但是,说实话,在 MySQL 中使用主外键的情况还真的是非常少。之前似乎有印象说 MySQL 不是很推荐通过主外键来建立表之间的联系。这个我们以后再详细学习 MySQL 相关的文章时再深入的学习。至于这件事是不是真的,我也只是仅有一个印象了,如果说得不对,也请大家不要见怪,后面学习到的时候我们再来纠正。

之所以要有外键这个东西,主要也是为了数据之前能够在数据库层面保持一定的关联,这样我们就可以做一些特殊的操作,比如说定义数据库的事件或者定时任务之类的,或者在关联删除的时候能够更加有效率。这样做的原因也正是为了保持数据的一致性和完整性。

当然,在 Laravel 中,可以不在数据库层面进行严格的设置,就可以在框架代码中实现主外键的关联。

代码语言:javascript复制
class MTest extends Model
{
    use HasFactory;
    protected $table = 'm_test';
    public $timestamps = false;

    public function gender(){
        return $this->belongsTo('AppModelsDbSex', 'sex');
    }
}

DbSex 模型是我们建立的针对 db_sex 表的模型,这个表是上篇文章中测试时使用的,就直接拿来使用了。在代码中,我们定义了一个方法,名为 gender() ,然后在里面 return 了一个 belongsTo() 方法。在这个方法中,第一个参数是指定要关联的模型,第二个参数是对应的字段。belongsTo 这个单词是什么意思呢?其实是 从属于 的意思,也就是说,我们当前这个模型的 sex 字段 从属于 db_sex 表。我们可以看下 belongsTo() 方法里面做了什么事情。

代码语言:javascript复制
// laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php
public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null)
{
    if (is_null($relation)) {
        $relation = $this->guessBelongsToRelation();
    }

    $instance = $this->newRelatedInstance($related);

    if (is_null($foreignKey)) {
        $foreignKey = Str::snake($relation).'_'.$instance->getKeyName();
    }

    $ownerKey = $ownerKey ?: $instance->getKeyName();

    return $this->newBelongsTo(
        $instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
    );
}

方法中上面的内容大家可以自己看看,主要我们需要关注的是最下面的 instance->newQuery() ,看出来没有,又是创建了一个 查询构造器 。然后通过

代码语言:javascript复制
// laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php
public function addConstraints()
{
    if (static::$constraints) {
        $table = $this->related->getTable();

        $this->query->where($table.'.'.$this->ownerKey, '=', $this->child->{$this->foreignKey});
    }
}

// select * from `db_sex` where `db_sex`.`id` = ?

这个 query 的 where 条件是什么意思?就是我们上面这条 SQL 语句的查询条件。它就是去查询 db_sex 表里面的数据,然后把获得的结果对象返回回来。至于这个 ? 里面的东西是什么,则是根据我们的 MTest 这个 Model 里面的 sex 字段的值来确定的,也就是 $this->child->{$this->foreignKey}这一段。$this->foreignKey 就是我们最上面代码中 belongsTo() 方法的第二个参数。这个参数是可选的,如果不填,它会默认找一个叫做 sex_id 的值,当然,在我们的数据中是没这个字段的,所以我们指定为 sex 。

最后整理下上面的调用链条,首先,我们生成定义的 MTest 是继承自 laravel/framework/src/Illuminate/Database/Eloquent/Model.php 这个抽象类的,注意,它是抽象类。然后,在这个抽象类中,使用了一个 laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php 特性,也就是 Trait 文件。在它的里面是 belongsTo() 方法的源码。接着,通过 newRelatedInstance() 方法实例化一个关系实例,也就是我们指定的 DbSex 模型的对象。然后就是调用 newBelongsTo() 方法生成一个 laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php 对象并进行查询,最后将这个对象返回回来。

接下来就是怎么使用的问题了。

代码语言:javascript复制
Route::get('model/test/relationship', function () {
    $id = (int)request()->get('id', 0);

    $info = AppModelsMTest::find($id);
    dump($info);
    dump($info->gender);
    dump($info->gender->name); // 女
});

使用这个关联的对象非常简单,直接调用 gender 属性就好。等等,不对呀,我们在模型里面定义的是一个 gender() 方法,怎么在外面使用的是一个属性?别急,我们再来看看源码,看看框架中是如何把调用属性变成调用一个方法的。

在 MTest 中,我们看不到什么东西,毕竟都是我们自己写的内容,所以我们需要来到它的基类,就是前端说过的那个抽象类 laravel/framework/src/Illuminate/Database/Eloquent/Model.php 。在这个类中,你会发现有不少魔术方法的使用,比如 __get() 这个方法。

代码语言:javascript复制
public function __get($key)
{
    return $this->getAttribute($key);
}

还记得这些魔术方法的作用吗?这些可是我最早期写的文章中介绍过的内容,如果不记得的小伙伴可以去到 【PHP的那些魔术方法(一)】https://mp.weixin.qq.com/s/QXCH0ZttxhuEBLQWrjB2_A 复习一下。这里我也不多做讲解了,反正如果是在对象调用的时候,调用的是没有明确在类模板中写下的属性,就会来到这个 __get() 魔术方法中。接下来的事情似乎就很好了办了吧,直接去 getAttribue() 方法中继续查看。

代码语言:javascript复制
// laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
public function getAttribute($key)
{
    if (! $key) {
        return;
    }

    if (array_key_exists($key, $this->attributes) ||
        array_key_exists($key, $this->casts) ||
        $this->hasGetMutator($key) ||
        $this->isClassCastable($key)) {
        return $this->getAttributeValue($key);
    }

    if (method_exists(self::class, $key)) {
        return;
    }

    return $this->getRelationValue($key);
}

这个 getAttribute() 方法又是在 Model 抽象类的另一个 Trait 中定义的。其实这段代码已经很清楚明了了,如果没有 key 就返回一个空的内容,如果 key 存在于当前这个模型类的相关属性中,则调用一些处理方法后返回。接下来,如果这个 key 是 Model 基类中的某个方法时,直接返回一个空的内容。注意,这里又用到了我们之前学习过的一个技巧,大家能看出来吗?它判断的是这个 key 是否是抽象基类 laravel/framework/src/Illuminate/Database/Eloquent/Model.php ,而不是我们定义的 MTest ,用的是一个 self 。相信一直陪伴着我学习的小伙伴马上就清楚了,【后期静态绑定在PHP中的使用】https://mp.weixin.qq.com/s/N0rlafUCBFf3kZlRy5btYA 好好复习一下吧。

最后,就来到了 getRelationValue() 方法。

代码语言:javascript复制
// laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
public function getRelationValue($key)
{
    if ($this->relationLoaded($key)) {
        return $this->relations[$key];
    }

    if (method_exists($this, $key) ||
        (static::$relationResolvers[get_class($this)][$key] ?? null)) {
        return $this->getRelationshipFromMethod($key);
    }
}

注意看,这里的 method_exsits() 中的参数变成什么了?没错,还是后期静态绑定的作用,这里使用了 $this ,现在这里指的对象就是 MTest 了,这一段没毛病吧,完美的后期静态绑定的应用。

最后的最后,来到 getRelationshipFromMethod() 方法中。

代码语言:javascript复制
// laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
protected function getRelationshipFromMethod($method)
{
    $relation = $this->$method();

    if (! $relation instanceof Relation) {
        if (is_null($relation)) {
            throw new LogicException(sprintf(
                '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method
            ));
        }

        throw new LogicException(sprintf(
            '%s::%s must return a relationship instance.', static::class, $method
        ));
    }

    return tap($relation->getResults(), function ($results) use ($method) {
        $this->setRelation($method, $results);
    });
}

$relation 变量首先执行我们定义的那个 gender() 方法获得返回的结果,也就是获取上面的 BelongsTo() 对象。然后来到最后的 tap() 中,tap() 是一个 Laravel 框架中定义的全局函数,和 env() 函数在一起的,它的作用是将第一个参数当作第二个参数的参数传递给第二参数,并执行第二个参数后,将第一个参数再返回回来。有点绕是不是?其实就是第一个参数是一个值,然后把它放到第二个参数中,这个参数是一个回调函数,然后通过回调函数来使用这个值进行其它的操作。这一段可能说得不太清楚,大家可以自己查看源代码然后调试一下就明白了。

在这段代码中,就是先调用 BelongsTo 对象的 getResults() 方法,获得关联的真正的 DbSex 这个 Model 对象,然后通过回调函数中的 setRelation() 绑定到 laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php 这个 Trait 的 $relations 属性中,方便后续的使用。最后 tap() 函数还是会把之前传递进行去的第一个参数的值,也就是最终的那个 DbSex 对象再一路返回到 __get() 中,这样,就完成了整个链条的调用。

总结

今天,我们学习的内容是 ORM 的概念以及基础的模型的使用,另外还加了一个关联功能的源码分析。当然,这只是最简单的一种一对一的关联,Laravel 框架中还可以实现非常复杂的关联,包括一对多,多对一,多对多的关联,这些内容还是大家自己研究怎么使用吧,毕竟我们文章的主旨还是在于搞清楚它们是怎么运行的,毕竟原理都是想通的,其它大家有兴趣的可以自己继续深入地分析。下篇文章我们还将继续进行模型的学习以及整个模型的源码分析。

参考文档:

https://learnku.com/docs/laravel/8.x/eloquent/9406

0 人点赞