Laravel - 与关系的收集需要很多时间

Posted

技术标签:

【中文标题】Laravel - 与关系的收集需要很多时间【英文标题】:Laravel - Collection with relations take a lot of time 【发布时间】:2018-01-01 13:57:49 【问题描述】:

我们正在使用 LUMEN 开发一个 API。 今天,我们在收集“TimeLog”模型时遇到了一个困惑的问题。 我们只是想从板模型和任务模型中获取所有带有附加信息的时间日志。 在一行时间日志中,我们有一个 board_id 和一个 task_id。两者是 1:1 的关系。

这是我们获取全部数据的第一个代码。这花了很多时间,有时我们会超时: BillingController.php

public function byYear() 

       $timeLog = TimeLog::get(); 

        $resp = array(); 

        foreach($timeLog->toArray() as $key => $value)   

            if(($timeLog[$key]->board_id && $timeLog[$key]->task_id) > 0 )       

                 array_push($resp, array(
                    'board_title' => isset($timeLog[$key]->board->title) ? $timeLog[$key]->board->title : null,
                    'task_title' => isset($timeLog[$key]->task->title) ? $timeLog[$key]->task->title : null,
                    'id' => $timeLog[$key]->id
                )); 
            
        


        return response()->json($resp);
       

建立关系的TimeLog.php

public function board()
        
            return $this->belongsTo('App\Board', 'board_id',  'id');
        

        public function task()
        
            return $this->belongsTo('App\Task', 'task_id',  'id');
        

我们的新方式是这样的: BillingController.php

 public function byYear() 



            $timeLog = TimeLog::
join('oc_boards', 'oc_boards.id', '=', 'oc_time_logs.board_id')
                            ->join('oc_tasks', 'oc_tasks.id', '=', 'oc_time_logs.task_id')
                            ->join('oc_users', 'oc_users.id', '=', 'oc_time_logs.user_id')
                            ->select('oc_boards.title AS board_title', 'oc_tasks.title AS task_title','oc_time_logs.id','oc_time_logs.time_used_sec','oc_users.id AS user_id')
                            ->getQuery()
                            ->get(); 

            return response()->json($timeLog);
           

我们删除了 TimeLog.php 中的关系,因为我们不再需要它了。现在我们有大约 1 秒的加载时间,这很好! 时间日志表中有大约 20k 个条目。

我的问题是:

    为什么第一种方法超出范围(什么导致超时?) 什么是 getQuery();到底是什么?

如果您需要更多信息,请询问我。

【问题讨论】:

我说:使用github.com/barryvdh/laravel-debugbar 或github.com/recca0120/laravel-tracy(或任何其他可以显示类似信息的工具)——它们会告诉你执行了哪些SQL 命令。我敢打赌,在第一个版本中,您将有数百个甚至数千个 SQL 请求,而在第二个版本中,它可能只有一个。至于这么多的查询——见laravel.com/docs/5.4/eloquent-relationships#eager-loading 谢谢! - 我们现在明白了。你知道什么是getQuery();到底在做什么? 在您的情况下可能没有任何内容(“获取基础查询构建器实例” - 即使您省略它也可能有效).. 或者只是“获取关系的基础查询”。简而言之——我无法正确回答这个问题(另外,对于复杂的查询,我更喜欢原始 SQL——更容易在 SQL 编辑器中编辑/调试等)。 【参考方案1】:

--第一个问题--

您可能面临的一个问题是内存中有大量数据,即:

$timeLog = TimeLog::get();

这已经是巨大的了。然后当您尝试将集合转换为数组时:

    集合中有一个循环。 根据我的理解在初始化循环时使用 $timeLog->toArray() 效率不高(不过我可能对此并不完全正确) 为检索相关模型进行了数千次查询

所以我建议的是五种方法(一种可以让您免于数百次查询),最后一种可以有效地返回定制的结果:

    既然你有很多数据,那么chunk 结果参考:Laravel chunk 所以你有这个:

    $timeLog = TimeLog::chunk(1000, function($logs)
        foreach ($logs as $log) 
        // Do the stuff here
        
    ); 
    

    另一种方法是使用游标(只运行一个条件匹配的查询)游标的内部操作正如理解的那样使用Generators。

    foreach (TimeLog::where([['board_id','>',0],['task_id', '>', 0]])->cursor() as $timelog) 
      //do the other stuffs here
    
    

    这看起来像第一个,但您已经将查询范围缩小到您需要的范围:

    TimeLog::where([['board_id','>',0],['task_id', '>', 0]])->get()
    

    Eager Loading 已经可以即时呈现您需要的关系但也可能会在内存中产生更多数据。因此,chunk 方法可能会使事情更容易管理(即使您预先加载相关模型)

    TimeLog::with(['board','task'],  function ($query) 
        $query->where([['board_id','>',0],['task_id', '>', 0]]);
    ])->get();
    

    您可以简单地使用Transformer

    使用转换器,即使大小很大,您也可以以优雅、干净和更可控的方法加载相关模型,另一个更大的好处是您可以转换结果而不必担心如何循环它 您可以简单地参考this answer,以便对其进行简单的使用。但是,如果您不需要转换您的响应,那么您可以采取其他选择。

虽然这可能不能完全解决问题,但是由于您面临的主要问题是基于内存管理的,所以上述方法应该很有用。

--第二个问题--

基于 Laravel API here 你可以看到:

它只返回底层查询构建器实例。据我观察,根据您的示例,不需要它。

更新

对于问题 1,由于您似乎只想将结果作为响应返回,因此,对结果进行分页更有效。 Laravel offers pagination 其中最简单的是 SimplePaginate,它很好。唯一的问题是它对数据库进行了更多查询,会检查最后一个索引;我猜它也使用cursor,但不确定。我想最后这可能更理想,有:

return TimeLog::paginate(1000);

【讨论】:

很好的答案。谢谢!【参考方案2】:

我也遇到过类似的问题。这里的主要问题是 Elloquent 在执行大量任务时真的很慢,因为它会同时获取所有结果,所以简短的回答是使用 PDO fetch 逐行获取它。

简短示例:

$db = DB::connection()->getPdo();

$query_sql = TimeLog::join('oc_boards', 'oc_boards.id', '=', 'oc_time_logs.board_id')
                            ->join('oc_tasks', 'oc_tasks.id', '=', 'oc_time_logs.task_id')
                            ->join('oc_users', 'oc_users.id', '=', 'oc_time_logs.user_id')
                            ->select('oc_boards.title AS board_title', 'oc_tasks.title AS task_title','oc_time_logs.id','oc_time_logs.time_used_sec','oc_users.id AS user_id')
                            ->toSql();

$query = $db->prepare($query->sql);
$query->execute();
$logs = array();
 while ($log = $query->fetch()) 
   $log_filled = new TimeLog();
   //fill your model and push it into an array to parse it to json in future
   array_push($logs,$log_filled);

return response()->json($logs);

【讨论】:

感谢您的回复。也感谢您提供详细信息。您是否阅读了我们的解决方案? - 这是最好的方法,还是你认为你的例子更好? 不客气。是的,我读过它。在我们的场景中,我们从一个大表中获取约 400-500k 行并将它们也作为 json 检索。当我们用 elloquent 尝试它时,只需 $objects = TableName::all(); return response()->json($objects) 关闭服务器,但使用 PDO fetch 它可以在不到 2 秒的时间内提供整个数据 @aaron0207 为什么不使用 chunk(),会不会造成更多开销? 您在前面进行连接的想法是正确的。然而,说 Eloquent 方式很慢是一种误导,因为它会预先获取所有数据,而这种方式是一次一个。整个结果集总是存储在内存中(在 php-land 或数据库中)。您的方法有效的原因是因为它确实一次将所有内容拉入 php 内存,而不是 OPs 查询将一个表拉入内存然后为每个成员的每个连接发出单独的请求。您也没有提到 OP 不必要地将数组增加了三倍(您的只有双倍)

以上是关于Laravel - 与关系的收集需要很多时间的主要内容,如果未能解决你的问题,请参考以下文章

在 laravel npm 运行开发错误:

Laravel - 雄辩的关系有很多但也有一个?

Laravel API JSON 自定义和表关系

Laravel 关系,需要建议

与同一张表的关系(多对多?) - Laravel

Laravel 需要 Mcrypt PHP 扩展