Laravel 模型操作中一次奇妙踩坑经历

@这是小豪的第十三篇文章

最近被 Laravel 模型中的一些小问题折腾的死去活来的,明明看着很清晰很明了的代码,却偏偏不能实现功能,现在带大家来切身经历一下这次奇妙的踩坑经历,代码看似很多,实则不多,大家别急着跑,哈哈。

准备

需求: 获取项目下的所有任务,且需要合并公共任务

逻辑关系:

  • 一个项目有很多任务
  • 一个项目有很多项目成员
  • 一个任务有一个执行人 (当任务类型为:1 的时候为公共事务)
  • 一个人有多个项目
  • 一个人有多个任务

前端所需数据格式如下:

{
    "user1": {
        "id": 1,
        "name": "Lhao",
        "email": "lhao@qq.com",
        "email_verified_at": null,
        "created_at": null,
        "updated_at": null,
        "pivot": {
            "project_id": 1,
            "user_id": 1
        },
        "tasks": [
            {
                "id": 1,
                "project_id": 1,
                "user_id": 1,
                "type": 0,
                "name": "task 1",
                "created_at": null,
                "updated_at": null
            }
            ...
        ]
    },
    "user2": {
            ...
    }
}

那我们现在来看看需要用到的各个模型,其中的各种对应关系我就不做讲解了哈,上面也有介绍,不太清楚的建议把模型关联再去细读一遍:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Project extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name',
    ];

    public function users()
    {  
        return $this->belongsToMany(User::class);
    }

    public function tasks()
    {  
        return $this->hasMany(Task::class);
    }
}
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'user_id',
        'name',
    ];

    const COMMON_TASK_TYPE = 1;

    public function scopeOfCommonTask(Builder $query)
    {
        return $query->where('type', self::COMMON_TASK_TYPE);
    }

    public function users()
    {  
        return $this->belongsTo(User::class);
    }

    public function project()
    {
        return $this->belongsTo(Project::class);
    }
}
...

class User extends Model
{
    ...

    public function projects()
    {  
        return $this->belongsToMany(Project::class);
    }

    public function tasks()
    {  
        return $this->hasMany(Task::class);
    }

    ...
}

开始

从上面的需求中大家可能会说,获取项目下的所有任务和公共事务直接通过:

$projectTasks = $project->tasks->merge(Task::ofCommonTask()->get())->groupBy('user_id');

这样不就可以了吗,但是这样有个问题就是数据格式不是前端所需要的,如果我们要转化成上面的格式的话,还需要获取用户数据然后将上面查询出来的数据塞进去,不太想这么干,不够优雅,哈哈,我打算通过项目获取到项目成员然后再加载任务数据,最后整合进公共任务,话不多说上代码:

public static function getProjectUserTasks(Project $project)
{
    $userTasks = $project->users->load(['tasks' => function ($query) use ($project) {
        $query->where('project_id', $project->id);
    }])->keyBy('name');

    // 不太清楚的 请看 scope 相关知识
    $commonTasks = Task::ofCommonTask()->get();

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->tasks = $userTask->tasks->merge($commonTasks);

        return $userTask;
    });

    return $userTasks;
}

看上面的代码是不是感觉很清爽,很直接,但是... 返回的数据是没有整合进 commonTask 的,这是为什么呢,明明 $userTask->tasks->merge($tasks) 也赋值了呀,问题出在哪里呢,我们测试一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->tasks = $userTask->tasks->merge($commonTasks);

        dd($userTask->tasks->toArray(), $userTask->toArray());

        return $userTask;
    });

    ....

具体的数据打印结果我就不贴出来了哈,占地方,哈哈,我直接说结果。

从打印的结果中可以看到 $userTask->tasks 中是有合并之后的数据的,但是 $userTask 还是原先的数据。这是为啥,我有点懵了,难道说 $userTask->tasks 操作是关联查询操作了?($userTask 是一个 User 对象集合,$userTask->tasks 会不会再次查询数据了?而不是直接获取的原有属性?),疑问出现了,我们就来测试看看:

    ...

    $userTasks = $project->users->load(['tasks' => function ($query) use ($project) {
        // $query->where('project_id', $project->id);
    }])->keyBy('name');

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        dd($userTask->tasks);
    });

    ....

通过对上面的测试发现,$userTask->tasks 是有携带上面查询条件的,所以说这个疑问排除了!

难道是集合属性不能这样赋值?我们再来测试一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->name = 111;

        return $userTasks;
    });

    ....

返回的结果是修改了的....

这就尴尬了,难道是对象集合中的非对象属性不能这样赋值?也不对呀,思来想去决定对对象本身做一个探索,直接在 map 中打印 $userTask :

Laravel 集合操作中一次奇妙踩坑经历

大家可以看到两个关键的属性:attributes、relations ,在实践中可以发现不管是 $userTask->name = "user**" 还是 $user->tasks = *** 的赋值操作都有对 attributes 做更改,这一点也可以从 Model 中的 __set 魔术方法中看到,其中是有调用一个 setAttribute 方法的,我们来看一下:

Laravel 集合操作中一次奇妙踩坑经历

Laravel 集合操作中一次奇妙踩坑经历

既然 attributes 被修改了,那究竟为啥在输出的时候只有他本身的属性有变更但是关联属性没有呢?

还记得我们刚才测试打印时候的 toArray 吗,就是他把对象集合转变成了一个数组,我们来看一下:

Laravel 集合操作中一次奇妙踩坑经历

明显看到 toArray 方法将 attributes 和 relations 转化成数组了,而且用的 array_merge 方法,大家知道相同 key 的时候,后面数组会覆盖前面数组,从前面的测试中可以看到 $userTask 中 attributes 是有变更,但是 relations 中的数据是没有发生任何变化的,这就可以解释为什么赋值 tasks 没有任何效果了,原有的数据覆盖掉了变更的数据。

所以我们现在要做的就是,对 relations 处理,那我们现在来看一下直接对 relations 处理是否有用:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->relations['tasks'] = $userTask->tasks->merge($commonTasks);

        return $userTasks;
    });

    ....

测试结果很显然是成功的,但是大家可能会发现直接操作 relations 或许有些不妥,别急,Laravel 也给我们提供了这样一个方法:

Laravel 集合操作中一次奇妙踩坑经历

现在我们把代码优化一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        return $user->setRelation('tasks', $user->tasks->merge($commonTasks));;
    });

    ....

大公告成,可以说很优雅,哈哈,大家可能会问,你这直接返回了没有调用 toArray 啊,数据是怎么合并的怎么转换的?大家知道在控制器中直接 return 的时候,是会直接转化为 Json 数据格式的,模型中也相对应的有这么一个方法:

Laravel 模型操作中一次奇妙踩坑经历

Laravel 模型操作中一次奇妙踩坑经历

一步步走下来发现,最终还是调用了 toArray 。所以嘛,这次踩坑算是跨过去了,哈哈。不知道大家有没有理解,有需要改进的地方大家在评论区留言噢。

特别鸣谢: zIym 同学 (咱俩一起跨的坑,哈哈)

结束语

其实吧最初我也没有想这么多,想了很多其它的解决办法,但是都是治根不治本,到头来发现自己对 Laravel 模型的工作原理还是不熟悉,只存在简单的应用上面,所以呀还是得追根溯源,并不是把时间都浪费在尝试上面,多看看源码,会有想不到的收获,哈哈。

eightone # Lhao

本帖由系统于 3天前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 8
fantasticcat
$userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->jobs = $userTask->tasks->merge($commonTasks); // 这里名字不和关联方法名相同就OK
        return $userTask;
    });
2周前

@fantasticcat 换名字是可以的,但是这篇文章主要是想告诉大家直接对关联属性赋值为什么不行,理解清楚之后,怎么去操作就更加明了了,哈哈

2周前

@大毛 :see_no_evil:

2周前
overtrue

:+1:

1周前

题外话,为什么前端会让你给user1 user2作为key的对象,这个真不是后端人员胡搞毛搞然后前端妥协了吗?

1周前

感谢大佬分享 :+1:

3天前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!