【Laravel】12. 模型的预加载

390 阅读2分钟

作者:拾年之璐   公众号:知行研究院

1、预加载,就是解决关联查询中产生的N+1 次查询带来的资源消耗。

所谓 N+1 条,就是起初获取全部数据的 1 条和,遍历的 N 条。看下面的示例。

比如在下面的关联查询中,要获取所有书籍的作者(或拥有者),即通过获取所有的书籍,查询每个书籍对应的所有者:

//获取所有书籍列表
$books = Book::all();
//遍历每一本书
foreach ($books as $book) {
    //每一本书的关联用户的姓名(关联查询)
    DebugBar::info($book->user->username);
}

其执行的SQL为:

select * from `laravel_books`

select * from `laravel_users` where `laravel_users`.`id` = 19 limit 1

select * from `laravel_users` where `laravel_users`.`id` = 20 limit 1

select * from `laravel_users` where `laravel_users`.`id` = 21 limit 1

select * from `laravel_users` where `laravel_users`.`id` = 24 limit 1

select * from `laravel_users` where `laravel_users`.`id` = 25 limit 1

select * from `laravel_users` where `laravel_users`.`id` = 26 limit 1

select * from `laravel_users` where `laravel_users`.`id` = 27 limit 1

select * from `laravel_users` where `laravel_users`.`id` = 29 limit 1

......

通过调试器 Debugbar 中 SQL 语句的分析,发现包含十多条 SQL 语句。

这是因为,在关联查询时,每遍历一次(foreach循环)就会执行一遍 SQL 语句,导致性能欠佳。

2、使用 with() 关键字,进行预载入设置,提前将 SQL 整合,即可大大减少执行的SQL语句数目。如:

//with 关键字预载入
$books = Book::with('user')->get();
foreach ($books as $book) {
    DebugBar::info($book->user->username);
}

相比较于原来的例子,只增加了with关键字。

这是,执行的结果是一致的。而执行的SQL为:

select * from `laravel_books`

select * from `laravel_users` where `laravel_users`.`id` in (19, 20, 21, 24, 25, 26, 27, 29, 79)

此时的SQL 执行数目为:1+1 条。

另外,使用 with() 关键字也支持设置显示的列,比如:

$books = Book::with('user:id,username')->get();
foreach ($books as $book) {
    DebugBar::info($book->user);
}

其输出的结果,是个模型User,并且只包含两个属性:

image-20210208224821129

另外,使用 with() 关键字也支持数组,多个关联:with(['book', 'prifile' ])

3、如果每次都必须使用预载入进行关联查询,可以在模型中定义:

即在books模型中,写入:

protected $with = ['user'];

然后就可以像上面的第一种方法使用(不需要加with()方法),即可自动使用预加载。

4、也可以使用闭包的方式,进行预载入筛选查询:

//闭包查询
$books = Book::with(['user' => function ($query) {
    $query->where('id', 19);//这样写可能没啥意义,看结果吧
}])->get();

//输出
foreach ($books as $book) {
    DebugBar::info($book->user);
}

//可以增加个为空的判断:
foreach ($books as $book) {
    if($book->user != null){
        DebugBar::info($book->user->username);
    }    
}

首先看其执行的SQL语句:

select * from `laravel_books`

select * from `laravel_users` where `laravel_users`.`id` in (19, 20, 21, 24, 25, 26, 27, 29, 79) and `id` = 19

然后其执行结果为:

image-20210208225442854

预载入筛选不可以使用limit、take 方法。

5、在上面的所有示例中,预加载是回提前进行管理执行的。

但有时,可能会产生逻辑判断,进而判断是否查询数据。为了避免自愿性能的浪费,可以采用延迟预载入

$books = Book::all();//这里的with()方法省了,用下面的load延迟加载。

if (true) {
    $books = $books->load('user'); 
    //这个load也可以使用闭包,如:
    //load(['user' => function () {
    //	条件信息
    //}])
    foreach ($books as $book) {
        DebugBar::info($book->user->username);
    }
}

6、使用loadCount()方法,可以实现延迟关联统计;

$users = User::all();
if (true) {
    return $users->loadCount('book');
}

执行的SQL为:

select * from `laravel_users`

select `id`, (select count(*) from `laravel_books` where `laravel_users`.`id` = `laravel_books`.`user_id`) as `book_count` from `laravel_users` where `laravel_users`.`id` in (19, 20, 21, 24, 25, 26, 27, 29, 76, 79, 80, 99, 100, 101, 106)

执行结果为:

[
    {
        "id": 19,
        "username": "蜡笔小新",
        ......
        "book_count": 5
    },
    {
        "id": 20,
        "username": "路飞",
        ......
        "book_count": 1
    },
    {
        "id": 21,
        "username": "黑崎一护",
        ......
        "book_count": 1
    },
    {
        "id": 24,
        "username": "小明",
        ......
        "book_count": 1
    },
    {
        "id": 25,
        "username": "孙悟饭",
        ......
        "book_count": 1
    },
    ......
]

以上。